diff --git a/DiffEngine.php b/DiffEngine.php
index 7f6ba68..f0c0d51 100644
--- a/DiffEngine.php
+++ b/DiffEngine.php
@@ -1102,8 +1102,8 @@ class DrupalDiffFormatter extends DiffFormatter {
   );
 
   function DrupalDiffFormatter() {
-    $this->leading_context_lines = variable_get('diff_context_lines_leading', 2);
-    $this->trailing_context_lines = variable_get('diff_context_lines_trailing', 2);
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
   }
 
   function _start_diff() {
@@ -1117,11 +1117,11 @@ class DrupalDiffFormatter extends DiffFormatter {
   function _block_header($xbeg, $xlen, $ybeg, $ylen) {
     return array(
       array(
-        'data' => theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
         'colspan' => 2,
       ),
       array(
-        'data' => theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
         'colspan' => 2,
       )
     );
@@ -1149,7 +1149,7 @@ class DrupalDiffFormatter extends DiffFormatter {
         'class' => 'diff-marker',
       ),
       array(
-        'data' => theme('diff_content_line', array('line' => $line)),
+        'data' => _theme('diff_content_line', array('line' => $line)),
         'class' => 'diff-context diff-addedline',
       )
     );
@@ -1165,7 +1165,7 @@ class DrupalDiffFormatter extends DiffFormatter {
         'class' => 'diff-marker',
       ),
       array(
-        'data' => theme('diff_content_line', array('line' => $line)),
+        'data' => _theme('diff_content_line', array('line' => $line)),
         'class' => 'diff-context diff-deletedline',
       )
     );
@@ -1178,7 +1178,7 @@ class DrupalDiffFormatter extends DiffFormatter {
     return array(
       '&nbsp;',
       array(
-        'data' => theme('diff_content_line', array('line' => $line)),
+        'data' => _theme('diff_content_line', array('line' => $line)),
         'class' => 'diff-context',
       )
     );
@@ -1187,7 +1187,7 @@ class DrupalDiffFormatter extends DiffFormatter {
   function emptyLine() {
     return array(
       '&nbsp;',
-      theme('diff_empty_line', array('line' => '&nbsp;')),
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
     );
   }
 
@@ -1266,7 +1266,7 @@ class DrupalDiffInline {
               $output .= $piece;
             }
             else {
-              $output .= theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
             }
           }
           break;
@@ -1277,7 +1277,7 @@ class DrupalDiffInline {
               $output .= $piece;
             }
             else {
-              $output .= theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
             }
           }
           break;
diff --git a/config/install/diff.settings.yml b/config/install/diff.settings.yml
new file mode 100644
index 0000000..89eef42
--- /dev/null
+++ b/config/install/diff.settings.yml
@@ -0,0 +1,14 @@
+diff_show_header_node: 1
+diff_show_header_user: 1
+diff_context_lines_trailing: 2
+diff_theme: default
+diff_radio_behavior: simple
+diff_context_lines_leading: 2
+diff_script_revisioning: simple
+diff_leading_context_lines: 2
+diff_trailing_context_lines: 2
+diff_admin_path_node: 1
+diff_show_diff_inline_node_bundles: null
+diff_additional_options_node: null
+diff_additional_options_user: null
+diff_default_state_node: raw
diff --git a/diff.admin.inc b/diff.admin.inc
index 8c5202f..b0c4202 100644
--- a/diff.admin.inc
+++ b/diff.admin.inc
@@ -12,7 +12,7 @@ function diff_admin_settings($form, $form_state) {
   $form['diff_theme'] = array(
     '#type' => 'select',
     '#title' => t('CSS options'),
-    '#default_value' => variable_get('diff_theme', 'default'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_theme'),
     '#options' => array(
       'default' => t('Classic'),
       'boxes' => t('Boxes'),
@@ -23,7 +23,7 @@ function diff_admin_settings($form, $form_state) {
   $form['diff_radio_behavior'] = array(
     '#type' => 'select',
     '#title' => t('Diff radio behavior'),
-    '#default_value' => variable_get('diff_radio_behavior', 'simple'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_radio_behavior'),
     '#options' => array(
       'simple' => t('Simple exclusion'),
       'linear' => t('Linear restrictions'),
@@ -32,19 +32,19 @@ function diff_admin_settings($form, $form_state) {
     '#description' => t('<em>Simple exclusion</em> means that users will not be able to select the same revision, <em>Linear restrictions</em> means that users can only select older or newer revisions of the current selections.'),
   );
 
-  $options = drupal_map_assoc(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+  $options = array_combine(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
   $form['diff_context_lines_leading'] = array(
     '#type' => 'select',
     '#title' => t('Leading context lines'),
     '#description' => t('This governs the number of unchanged leading context "lines" to preserve.'),
-    '#default_value' => variable_get('diff_context_lines_leading', 2),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_context_lines_leading'),
     '#options' => $options,
   );
   $form['diff_context_lines_trailing'] = array(
     '#type' => 'select',
     '#title' => t('Trailing context lines'),
     '#description' => t('This governs the number of unchanged trailing context "lines" to preserve.'),
-    '#default_value' => variable_get('diff_context_lines_trailing', 2),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_context_lines_trailing'),
     '#options' => $options,
   );
 
@@ -55,14 +55,14 @@ function diff_admin_settings($form, $form_state) {
  * Global entity settings.
  */
 function diff_admin_global_entity_settings($form, $form_state, $entity_type) {
-  $entity_info = entity_get_info($entity_type);
+  $entity_info = \Drupal::entityManager()->getDefinition($entity_type);
   drupal_set_title(t('Diff settings for %entity_label entities', array('%entity_label' => $entity_info['label'])), PASS_THROUGH);
 
   if ($options = module_invoke_all('entity_diff_options', $entity_type)) {
     $form['diff_additional_options_' . $entity_type] = array(
       '#type' => 'checkboxes',
       '#title' => t('Property options'),
-      '#default_value' => variable_get('diff_additional_options_' . $entity_type, array()),
+      '#default_value' => \Drupal::config('diff.settings')->get('diff_additional_options_' . $entity_type),
       '#options' => $options,
     );
   }
@@ -76,9 +76,9 @@ function diff_admin_global_entity_settings($form, $form_state, $entity_type) {
   $form['diff_show_header_' . $entity_type] = array(
     '#type' => 'checkbox',
     '#title' => t('Show entity label row header'),
-    '#default_value' => variable_get('diff_show_header_' . $entity_type, 1),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_show_header_' . $entity_type),
   );
-  if (user_access('administer permissions')) {
+  if (\Drupal::currentUser()->hasPermission('administer permissions')) {
     $admin_link = l(t('View the administration theme'), 'admin/people/permissions', array('fragment' => 'edit-view-the-administration-theme'));
   }
   else {
@@ -88,12 +88,12 @@ function diff_admin_global_entity_settings($form, $form_state, $entity_type) {
     '#type' => 'checkbox',
     '#title' => t('Use administration theme'),
     '#description' => t('This option will enable users with the <em>!link</em> permission to use the admin theme when doing comparisons.', array('!link' => $admin_link)),
-    '#default_value' => variable_get('diff_admin_path_' . $entity_type, 1),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_admin_path_' . $entity_type),
   );
   $form['diff_default_state_' . $entity_type] = array(
     '#type' => 'select',
     '#title' => t('Diff default state'),
-    '#default_value' => variable_get('diff_default_state_' . $entity_type, 'raw'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_default_state_' . $entity_type),
     '#options' => array(
       'raw' => t('HTML view'),
       'raw_plain' => t('Plain view'),
@@ -116,7 +116,7 @@ function diff_admin_field_overview() {
   $rows = array();
 
   // Skip field types which have no widget types.
-  $field_types = field_info_field_types();
+  $field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
   $widgets = array();
   foreach (field_info_widget_types() as $name => $widget_type) {
     foreach ($widget_type['field types'] as $widget_field_type) {
@@ -154,7 +154,7 @@ function diff_admin_field_overview() {
 function diff_admin_global_field_settings($form, $form_state, $type) {
   module_load_include('diff.inc', 'diff');
 
-  $field_types = field_info_field_types();
+  $field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
   if (!isset($field_types[$type])) {
     drupal_set_message(t('Invalid field type.'), 'error');
     drupal_goto('admin/config/content/diff/fields');
@@ -167,7 +167,7 @@ function diff_admin_global_field_settings($form, $form_state, $type) {
   )), PASS_THROUGH);
 
   $variable_name = "diff_{$field_type['module']}_field_{$type}_default_options";
-  $settings = variable_get($variable_name, array());
+  $settings = \Drupal::config('diff.settings')->get($variable_name);
   $settings = _diff_field_default_settings($field_type['module'], $type, $settings);
   $func = $field_type['module'] . '_field_diff_options_form';
   if (function_exists($func) && ($options_form = $func($type, $settings))) {
diff --git a/diff.diff.inc b/diff.diff.inc
index 50f99cb..e84e223 100644
--- a/diff.diff.inc
+++ b/diff.diff.inc
@@ -28,7 +28,7 @@ function diff_entity_diff($old_entity, $new_entity, $context) {
   $return = array();
 
   $entity_type = $context['entity_type'];
-  $info = entity_get_info($entity_type);
+  $info = \Drupal::entityManager()->getDefinition($entity_type);
   if (!empty($info['fieldable'])) {
     $return = diff_entity_fields_diff($old_entity, $new_entity, $context);
   }
@@ -130,7 +130,7 @@ function diff_entity_fields_diff($old_entity, $new_entity, $context, $default_la
         $func($old_items, $new_items, $field_context);
       }
       // Allow other modules to act safely on behalf of the core field module.
-      drupal_alter('field_diff_view_prepare', $old_items, $new_items, $field_context);
+      \Drupal::moduleHandler()->alter('field_diff_view_prepare', $old_items, $new_items, $field_context);
 
       // These functions compiles the items into comparable arrays of strings.
       $func = $field['module'] . '_field_diff_view';
@@ -147,8 +147,8 @@ function diff_entity_fields_diff($old_entity, $new_entity, $context, $default_la
       $new_values = $func($new_items, $new_context);
 
       // Allow other modules to act safely on behalf of the core field module.
-      drupal_alter('field_diff_view', $old_values, $old_items, $old_context);
-      drupal_alter('field_diff_view', $new_values, $new_items, $new_context);
+      \Drupal::moduleHandler()->alter('field_diff_view', $old_values, $old_items, $old_context);
+      \Drupal::moduleHandler()->alter('field_diff_view', $new_values, $new_items, $new_context);
 
       $max = max(array(count($old_values), count($new_values)));
       if ($max) {
@@ -208,7 +208,7 @@ function diff_field_diff_view($items, $context) {
    $view_mode = empty($context['view_mode']) ? 'diff_standard' : $context['view_mode'];
    $element = field_view_field($context['entity_type'], $entity, $context['field']['field_name'], $view_mode, $langcode);
 
-   foreach (element_children($element) as $delta) {
+   foreach (\Drupal\Core\Render\Element::children($element) as $delta) {
      $diff_items[$delta] = drupal_render($element[$delta]);
    }
   return $diff_items;
@@ -231,12 +231,12 @@ function diff_get_field_settings($field_context) {
   $field = $field_context['field'];
 
   // Update saved settings from the global settings for this field type.
-  $settings = variable_get("diff_{$field['module']}_field_{$field['type']}_default_options", array());
+  $settings = \Drupal::config('diff.settings')->get("diff_{$field['module']}_field_{$field['type']}_default_options");
 
   $settings = _diff_field_default_settings($field['module'], $field['type'], $settings);
 
   // Allow modules to alter the field settings based on the current context.
-  drupal_alter('diff_field_settings', $settings, $field_context);
+  \Drupal::moduleHandler()->alter('diff_field_settings', $settings, $field_context);
 
   return $settings;
 }
@@ -343,12 +343,12 @@ function _diff_autoload($field_or_module) {
   $includes = &drupal_static(__FUNCTION__, FALSE);
   if (!$includes) {
     $includes = array(
-      'file' => module_exists('file'),
-      'image' => module_exists('image'),
-      'list' => module_exists('list'),
-      'taxonomy' => module_exists('taxonomy'),
-      'text' => module_exists('text'),
-      'number' => module_exists('number'),
+      'file' => \Drupal::moduleHandler()->moduleExists('file'),
+      'image' => \Drupal::moduleHandler()->moduleExists('image'),
+      'list' => \Drupal::moduleHandler()->moduleExists('list'),
+      'taxonomy' => \Drupal::moduleHandler()->moduleExists('taxonomy'),
+      'text' => \Drupal::moduleHandler()->moduleExists('text'),
+      'number' => \Drupal::moduleHandler()->moduleExists('number'),
     );
   }
 
diff --git a/diff.info b/diff.info
deleted file mode 100644
index cd95be6..0000000
--- a/diff.info
+++ /dev/null
@@ -1,6 +0,0 @@
-name = Diff
-description = Show differences between content revisions.
-core = 7.x
-configure = admin/config/content/diff
-
-files[] = DiffEngine.php
diff --git a/diff.info.yml b/diff.info.yml
new file mode 100644
index 0000000..66285d3
--- /dev/null
+++ b/diff.info.yml
@@ -0,0 +1,4 @@
+name: Diff
+description: 'Show differences between content revisions.'
+core: 8.x
+type: module
diff --git a/diff.install b/diff.install
index 3d1ac83..6952d70 100644
--- a/diff.install
+++ b/diff.install
@@ -25,13 +25,13 @@ function diff_uninstall() {
   }
 
   // Delete global variables.
-  variable_del('diff_context_lines_trailing');
-  variable_del('diff_context_lines_leading');
-  variable_del('diff_theme');
-  variable_del('diff_radio_behavior', '');
+  \Drupal::config('diff.settings')->clear('diff_context_lines_trailing');
+  \Drupal::config('diff.settings')->clear('diff_context_lines_leading');
+  \Drupal::config('diff.settings')->clear('diff_theme');
+  \Drupal::config('diff.settings')->clear('diff_radio_behavior', '');
 
   foreach (field_info_fields() as $field) {
-    variable_del("diff_{$field['module']}_field_{$field['type']}_default_options");
+    \Drupal::config('diff.settings')->clear("diff_{$field['module']}_field_{$field['type']}_default_options");
   }
 }
 
@@ -47,10 +47,10 @@ function diff_update_7300() {
       'show_diff_inline',
     );
     foreach ($type_variables as $prefix) {
-      $setting = variable_get($prefix . '_' . $bundle, NULL);
+      $setting = \Drupal::config('diff.settings')->get($prefix . '_' . $bundle);
       if (isset($setting)) {
-        variable_del($prefix . '_' . $bundle);
-        variable_set('diff_' . $prefix . '_node_' . $bundle, $setting);
+        \Drupal::config('diff.settings')->clear($prefix . '_' . $bundle);
+        \Drupal::config('diff.settings')->set('diff_' . $prefix . '_node_' . $bundle, $setting);
       }
     }
   }
@@ -69,17 +69,17 @@ function diff_update_7300() {
  */
 function diff_update_7303() {
   // Get current values
-  $radio = variable_get('diff_script_revisioning', 'simple');
-  $leading = variable_get('diff_leading_context_lines', 2);
-  $trailing = variable_get('diff_trailing_context_lines', 2);
+  $radio = \Drupal::config('diff.settings')->get('diff_script_revisioning');
+  $leading = \Drupal::config('diff.settings')->get('diff_leading_context_lines');
+  $trailing = \Drupal::config('diff.settings')->get('diff_trailing_context_lines');
   // Create new variable names
-  variable_set('diff_radio_behavior', $radio);
-  variable_set('diff_context_lines_leading', $leading);
-  variable_set('diff_context_lines_trailing', $trailing);
+  \Drupal::config('diff.settings')->set('diff_radio_behavior', $radio);
+  \Drupal::config('diff.settings')->set('diff_context_lines_leading', $leading);
+  \Drupal::config('diff.settings')->set('diff_context_lines_trailing', $trailing);
   // Delete old variables
-  variable_del('diff_script_revisioning');
-  variable_del('diff_leading_context_lines');
-  variable_del('diff_trailing_context_lines');
+  \Drupal::config('diff.settings')->clear('diff_script_revisioning');
+  \Drupal::config('diff.settings')->clear('diff_leading_context_lines');
+  \Drupal::config('diff.settings')->clear('diff_trailing_context_lines');
 }
 
 /**
@@ -87,19 +87,19 @@ function diff_update_7303() {
  */
 function diff_update_7304() {
   // This is now always applied to text fields.
-  variable_del('diff_normalise_text');
+  \Drupal::config('diff.settings')->clear('diff_normalise_text');
 
   // Merge the content type settings for the inline diff block into a single variable.
   // diff_update_7300() - show_diff_inline_TYPE to diff_show_diff_inline_node_TYPE
   $node_types = array_keys(node_type_get_types());
   $enabled_types = array();
   foreach ($node_types as $node_type) {
-    if (variable_get('diff_show_diff_inline_node_' . $node_type, FALSE)) {
+    if (\Drupal::config('diff.settings')->get('diff_show_diff_inline_node_' . $node_type)) {
       $enabled_types[$node_type] = $node_type;
     }
-    variable_del('diff_show_diff_inline_node_' . $node_type);
+    \Drupal::config('diff.settings')->clear('diff_show_diff_inline_node_' . $node_type);
   }
-  variable_set('diff_show_diff_inline_node_bundles', $enabled_types);
+  \Drupal::config('diff.settings')->set('diff_show_diff_inline_node_bundles', $enabled_types);
 
   // Warn users that these settings are altered.
   drupal_set_message(t('Diff <em>Inline differences</em> content type settings are now located within the <em>Inline differences</em> block settings.'));
@@ -110,7 +110,7 @@ function diff_update_7304() {
  */
 function diff_update_7305() {
   // Rebuild the menus.
-  variable_set('menu_rebuild_needed', TRUE);
+  \Drupal::config('diff.settings')->set('menu_rebuild_needed', TRUE);
 
   // Removed the enforced entity view mode.
   db_delete('variable')
@@ -129,5 +129,5 @@ function diff_update_7305() {
  * still shows by default when doing node comparisons.
  */
 function diff_update_7306() {
-  variable_set('diff_additional_options_node', array('title' => 'title'));
+  \Drupal::config('diff.settings')->set('diff_additional_options_node', array('title' => 'title'));
 }
diff --git a/diff.links.menu.yml b/diff.links.menu.yml
new file mode 100644
index 0000000..68f5c28
--- /dev/null
+++ b/diff.links.menu.yml
@@ -0,0 +1,5 @@
+diff.admin_config_content_diff:
+  route_name: diff.admin_config_content_diff
+  title: Diff
+  parent: system.admin_config_content
+  description: 'Diff settings.'
diff --git a/diff.links.task.yml b/diff.links.task.yml
new file mode 100644
index 0000000..2b2ba3d
--- /dev/null
+++ b/diff.links.task.yml
@@ -0,0 +1,33 @@
+diff.node_revisions_view:
+  route_name: diff.node_revisions_view
+  title: 'Compare revisions'
+  parent_id: entity.node.version_history
+diff.node_revisions_view_latest:
+  route_name: diff.node_revisions_view_latest
+  title: 'Show latest difference'
+diff.admin_config_content_diff_fields:
+  route_name: diff.admin_config_content_diff_fields
+  title: Fields
+  parent_id: diff.admin_config_content_diff
+diff.admin_config_content_diff_entities:
+  route_name: diff.admin_config_content_diff_entities
+  title: Entities
+  parent_id: diff.admin_config_content_diff
+diff.admin_config_content_diff_entities_user:
+  route_name: diff.admin_config_content_diff_entities_user
+  title: Users
+  parent_id: diff.admin_config_content_diff_entities
+diff.node_revisions_list:
+  route_name: entity.node.version_history
+  title: 'List revisions'
+  base_route: diff.node_revisions_list
+diff.admin_config_content_diff_settings:
+  route_name: diff.admin_config_content_diff
+  title: Settings
+  weight: -10
+  base_route: diff.admin_config_content_diff_settings
+diff.admin_config_content_diff_entities_node:
+  route_name: diff.admin_config_content_diff_entities
+  title: Nodes
+  weight: -10
+  base_route: diff.admin_config_content_diff_entities_node
diff --git a/diff.module b/diff.module
index c4010f6..ad2efda 100644
--- a/diff.module
+++ b/diff.module
@@ -193,7 +193,7 @@ function diff_menu_alter(&$callbacks) {
  */
 function diff_admin_paths_alter(&$paths) {
   // By default, treat all diff pages as administrative.
-  if (variable_get('diff_admin_path_node', 1)) {
+  if (\Drupal::config('diff.settings')->get('diff_admin_path_node')) {
     $paths['node/*/revisions/view/*/*'] = TRUE;
   }
 }
@@ -202,7 +202,7 @@ function diff_admin_paths_alter(&$paths) {
  * Access callback for the node revisions page.
  */
 function diff_node_revision_access($node, $op = 'view') {
-  $may_revision_this_type = variable_get('diff_enable_revisions_page_node_' . $node->type, TRUE) || user_access('administer nodes');
+  $may_revision_this_type = \Drupal::config('diff.settings')->get('diff_enable_revisions_page_node_' . $node->type) || \Drupal::currentUser()->hasPermission('administer nodes');
   return $may_revision_this_type && _node_revision_access($node, $op);
 }
 
@@ -268,7 +268,7 @@ function diff_block_configure($delta = '') {
       $form['bundles'] = array(
         '#type' => 'checkboxes',
         '#title' => t('Enabled content types'),
-        '#default_value' => variable_get('diff_show_diff_inline_node_bundles', array()),
+        '#default_value' => \Drupal::config('diff.settings')->get('diff_show_diff_inline_node_bundles'),
         '#options' => node_type_get_names(),
         '#description' => t('Show this block only on pages that display content of the given type(s).'),
       );
@@ -283,7 +283,7 @@ function diff_block_configure($delta = '') {
 function diff_block_save($delta = '', $edit = array()) {
   switch ($delta) {
     case 'inline':
-      variable_set('diff_show_diff_inline_node_bundles', $edit['bundles']);
+      \Drupal::config('diff.settings')->set('diff_show_diff_inline_node_bundles', $edit['bundles']);
       break;
   }
 }
@@ -292,8 +292,8 @@ function diff_block_save($delta = '', $edit = array()) {
  * Implements hook_block_view().
  */
 function diff_block_view($delta) {
-  if ($delta === 'inline' && user_access('view revisions') && ($node = menu_get_object()) && arg(2) !== 'edit') {
-    $enabled_types = variable_get('diff_show_diff_inline_node_bundles', array());
+  if ($delta === 'inline' && \Drupal::currentUser()->hasPermission('view revisions') && ($node = menu_get_object()) && arg(2) !== 'edit') {
+    $enabled_types = \Drupal::config('diff.settings')->get('diff_show_diff_inline_node_bundles');
     if (!empty($enabled_types[$node->type])) {
       $block = array();
       $revisions = node_revision_list($node);
@@ -311,7 +311,7 @@ function diff_block_view($delta) {
  */
 function diff_node_view_alter(&$build) {
   $node = $build['#node'];
-  if (user_access('view revisions') && in_array($node->type, variable_get('diff_show_diff_inline_node_bundles', array()))) {
+  if (\Drupal::currentUser()->hasPermission('view revisions') && in_array($node->type, \Drupal::config('diff.settings')->get('diff_show_diff_inline_node_bundles'))) {
     // Ugly but cheap way to check that we are viewing a node's revision page.
     if (arg(2) === 'revisions' && arg(3) === $node->vid) {
       module_load_include('inc', 'diff', 'diff.pages');
@@ -326,10 +326,10 @@ function diff_node_view_alter(&$build) {
 /**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
-function diff_form_node_form_alter(&$form, $form_state) {
+function diff_form_node_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
   // Add a 'View changes' button on the node edit form.
   $node = $form['#node'];
-  if (variable_get('diff_show_preview_changes_node_' . $node->type, TRUE) && !empty($node->nid)) {
+  if (\Drupal::config('diff.settings')->get('diff_show_preview_changes_node_' . $node->type) && !empty($node->nid)) {
     $form['actions']['preview_changes'] = array(
       '#type' => 'submit',
       '#value' => t('View changes'),
@@ -342,7 +342,7 @@ function diff_form_node_form_alter(&$form, $form_state) {
 /**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
-function diff_form_node_type_form_alter(&$form, $form_state) {
+function diff_form_node_type_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
   if (isset($form['type'])) {
     $type = $form['#node_type'];
     $form['diff'] = array(
@@ -355,16 +355,16 @@ function diff_form_node_type_form_alter(&$form, $form_state) {
       '#type' => 'checkbox',
       '#title' => t('Show <em>View changes</em> button on node edit form'),
       '#weight' => 10,
-      '#default_value' => variable_get('diff_show_preview_changes_node_' . $type->type, TRUE),
+      '#default_value' => \Drupal::config('diff.settings')->get('diff_show_preview_changes_node_' . $type->type),
     );
     $form['diff']['diff_enable_revisions_page_node'] = array(
       '#type' => 'checkbox',
       '#title' => t('Enable the <em>Revisions</em> page for this content type'),
       '#weight' => 11,
-      '#default_value' => variable_get('diff_enable_revisions_page_node_' . $type->type, TRUE),
+      '#default_value' => \Drupal::config('diff.settings')->get('diff_enable_revisions_page_node_' . $type->type),
     );
     $options = array();
-    $info = entity_get_info('node');
+    $info = \Drupal::entityManager()->getDefinition('node');
     foreach ($info['view modes'] as $view_mode => $view_mode_info) {
       $options[$view_mode] = $view_mode_info['label'];
     }
@@ -374,7 +374,7 @@ function diff_form_node_type_form_alter(&$form, $form_state) {
       '#description' => t('Governs the <em>Current revision</em> view mode when doing standard comparisons.'),
       '#options' => $options,
       '#weight' => 13,
-      '#default_value' => variable_get('diff_view_mode_preview_node_' . $type->type, 'full'),
+      '#default_value' => \Drupal::config('diff.settings')->get('diff_view_mode_preview_node_' . $type->type),
       '#empty_value' => '',
       '#empty_option' => t('- Do not display -'),
     );
@@ -394,21 +394,21 @@ function diff_node_type_update($info) {
       'diff_view_mode_preview_node',
     );
     foreach ($type_variables as $prefix) {
-      $setting = variable_get($prefix . '_' . $info->old_type, NULL);
+      $setting = \Drupal::config('diff.settings')->get($prefix . '_' . $info->old_type);
       if (isset($setting)) {
-        variable_del($prefix . '_' . $info->old_type);
-        variable_set($prefix . '_' . $info->type, $setting);
+        \Drupal::config('diff.settings')->clear($prefix . '_' . $info->old_type);
+        \Drupal::config('diff.settings')->set($prefix . '_' . $info->type, $setting);
       }
     }
 
     // Block settings are combined in a single variable.
-    $inline_block_types = variable_get('diff_show_diff_inline_node_bundles', array());
+    $inline_block_types = \Drupal::config('diff.settings')->get('diff_show_diff_inline_node_bundles');
     if (isset($inline_block_types[$info->old_type])) {
       if (!empty($inline_block_types[$info->old_type])) {
         $inline_block_types[$info->type] = $info->type;
       }
       unset($inline_block_types[$info->old_type]);
-      variable_set('diff_show_diff_inline_node_bundles', $inline_block_types);
+      \Drupal::config('diff.settings')->set('diff_show_diff_inline_node_bundles', $inline_block_types);
     }
   }
 }
@@ -417,9 +417,9 @@ function diff_node_type_update($info) {
  * Implements hook_node_type_delete().
  */
 function diff_node_type_delete($info) {
-  variable_del('diff_show_preview_changes_node_' . $info->type);
-  variable_del('diff_enable_revisions_page_node_' . $info->type);
-  variable_del('diff_view_mode_preview_node_' . $info->type);
+  \Drupal::config('diff.settings')->clear('diff_show_preview_changes_node_' . $info->type);
+  \Drupal::config('diff.settings')->clear('diff_enable_revisions_page_node_' . $info->type);
+  \Drupal::config('diff.settings')->clear('diff_view_mode_preview_node_' . $info->type);
 }
 
 /**
@@ -436,7 +436,7 @@ function diff_node_form_build_preview_changes($form, &$form_state) {
   $rows = _diff_body_rows($old_node, $node);
 
   $header = _diff_default_header(t('Original'), t('Changes'));
-  $changes = theme('table__diff__preview', array(
+  $changes = _theme('table__diff__preview', array(
     'header' => $header,
     'rows' => $rows,
     'attributes' => array('class' => array('diff')),
@@ -612,13 +612,13 @@ function diff_filter_xss($string) {
  */
 function diff_build_attachments($jscript = FALSE) {
   $attachments = array();
-  $theme = variable_get('diff_theme', 'default');
+  $theme = \Drupal::config('diff.settings')->get('diff_theme');
   if ($theme) {
     $attachments['css'] = array(
       drupal_get_path('module', 'diff') . "/css/diff.{$theme}.css",
     );
   }
-  $type = variable_get('diff_radio_behavior', 'simple');
+  $type = \Drupal::config('diff.settings')->get('diff_radio_behavior');
   if ($jscript && $type) {
     $attachments['js'] = array(
       drupal_get_path('module', 'diff') . "/js/diff.js",
@@ -638,7 +638,7 @@ function node_entity_diff($old_node, $new_node, $context) {
   $result = array();
   if ($context['entity_type'] == 'node') {
     module_load_include('inc', 'diff', 'includes/node');
-    $options = variable_get('diff_additional_options_node', array('title' => 'title'));
+    $options = \Drupal::config('diff.settings')->get('diff_additional_options_node');
     foreach (node_entity_diff_options('node') as $key => $option_label) {
       if (!empty($options[$key])) {
         $func = '_node_entity_diff_additional_options_' . $key;
@@ -667,7 +667,7 @@ function node_entity_diff_options($entity_type) {
       'changed' => t('Updated date'),
       'revision_timestamp' => t('Revision timestamp'),
     );
-    if (module_exists('comment')) {
+    if (\Drupal::moduleHandler()->moduleExists('comment')) {
       $options['comment'] = t('Comment setting');
     }
     return $options;
@@ -681,7 +681,7 @@ function user_entity_diff($old_user, $new_user, $context) {
   $result = array();
   if ($context['entity_type'] == 'user') {
     module_load_include('inc', 'diff', 'includes/user');
-    $options = variable_get('diff_additional_options_user', array('name' => 'name', 'mail' => 'mail', 'status' => 'status'));
+    $options = \Drupal::config('diff.settings')->get('diff_additional_options_user');
     foreach (user_entity_diff_options('user') as $key => $option_label) {
       if (!empty($options[$key])) {
         $func = '_user_entity_diff_additional_options_' . $key;
diff --git a/diff.pages.inc b/diff.pages.inc
index 5d5bd8a..94f5317 100644
--- a/diff.pages.inc
+++ b/diff.pages.inc
@@ -58,11 +58,11 @@ function diff_node_revisions($form, $form_state, $node) {
   }
 
   $revert_permission = FALSE;
-  if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
+  if ((\Drupal::currentUser()->hasPermission('revert revisions') || \Drupal::currentUser()->hasPermission('administer nodes')) && node_access('update', $node)) {
     $revert_permission = TRUE;
   }
   $delete_permission = FALSE;
-  if ((user_access('delete revisions') || user_access('administer nodes')) && node_access('delete', $node)) {
+  if ((\Drupal::currentUser()->hasPermission('delete revisions') || \Drupal::currentUser()->hasPermission('administer nodes')) && node_access('delete', $node)) {
     $delete_permission = TRUE;
   }
 
@@ -75,7 +75,7 @@ function diff_node_revisions($form, $form_state, $node) {
       $form['info'][$revision->vid] = array(
         '#markup' => t('!date by !username', array(
           '!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"),
-          '!username' => theme('username', array('account' => $revision)))) . $revision_log,
+          '!username' => _theme('username', array('account' => $revision)))) . $revision_log,
       );
     }
     else {
@@ -83,7 +83,7 @@ function diff_node_revisions($form, $form_state, $node) {
       $form['info'][$revision->vid] = array(
         '#markup' => t('!date by !username', array(
           '!date' => $diff_date,
-          '!username' => theme('username', array('account' => $revision)))
+          '!username' => _theme('username', array('account' => $revision)))
         ) . $revision_log,
       );
       if ($revert_permission) {
@@ -121,7 +121,7 @@ function diff_node_revisions($form, $form_state, $node) {
   $form['submit'] = array('#type' => 'submit', '#value' => t('Compare'));
 
   if (count($revision_list) > REVISION_LIST_SIZE) {
-    $form['#suffix'] = theme('pager');
+    $form['#suffix'] = _theme('pager');
   }
   $form['#attached'] = diff_build_attachments(TRUE);
   return $form;
@@ -162,7 +162,7 @@ function diff_diffs_show($node, $old_vid, $new_vid, $state = NULL) {
   // Attaches the CSS.
   $build['#attached'] = diff_build_attachments();
 
-  $default_state = variable_get('diff_default_state_node', 'raw');
+  $default_state = \Drupal::config('diff.settings')->get('diff_default_state_node');
   if (empty($state)) {
     $state = $default_state;
   }
@@ -183,11 +183,11 @@ function diff_diffs_show($node, $old_vid, $new_vid, $state = NULL) {
   // Generate table header (date, username, log message).
   $old_header = t('!date by !username', array(
     '!date' => l(format_date($old_node->revision_timestamp), "node/$node->nid/revisions/$old_node->vid/view", array('absolute' => 1)),
-    '!username' => theme('username', array('account' => $node_revisions[$old_vid])),
+    '!username' => _theme('username', array('account' => $node_revisions[$old_vid])),
   ));
   $new_header = t('!date by !username', array(
     '!date' => l(format_date($new_node->revision_timestamp), "node/$node->nid/revisions/$new_node->vid/view", array('absolute' => 1)),
-    '!username' => theme('username', array('account' => $node_revisions[$new_vid])),
+    '!username' => _theme('username', array('account' => $node_revisions[$new_vid])),
   ));
 
   $old_log = $old_node->log != '' ? '<p class="revision-log">' . filter_xss($old_node->log) . '</p>' : '';
@@ -248,7 +248,7 @@ function diff_diffs_show($node, $old_vid, $new_vid, $state = NULL) {
     );
   }
   if (count($links) > 1) {
-    $state_links = theme('links', array(
+    $state_links = _theme('links', array(
       'links' => $links,
       'attributes' => array('class' => array('links', 'inline')),
     ));
@@ -272,7 +272,7 @@ function diff_diffs_show($node, $old_vid, $new_vid, $state = NULL) {
   );
 
   // Allow users to hide or set the display mode of the preview.
-  if (node_is_page($node) && $view_mode = variable_get('diff_view_mode_preview_node_' . $new_node->type, 'full')) {
+  if (node_is_page($node) && $view_mode = \Drupal::config('diff.settings')->get('diff_view_mode_preview_node_' . $new_node->type)) {
     $header = '';
     if ($node->vid == $new_vid) {
       $header .= '<div class="diff-section-title">' . t('Current revision:') . '</div>';
@@ -307,7 +307,7 @@ function diff_entity_body_rows($entity_type, $left_entity, $right_entity, $conte
   // This is an unique index only, so no need for drupal_static().
   static $table_row_counter = 0;
 
-  if ($theme = variable_get('diff_theme', 'default')) {
+  if ($theme = \Drupal::config('diff.settings')->get('diff_theme')) {
     drupal_add_css(drupal_get_path('module', 'diff') . "/css/diff.{$theme}.css");
   }
 
@@ -428,14 +428,14 @@ function diff_compare_entities($left_entity, $right_entity, $context) {
   $diff = module_invoke_all('entity_diff', $left_entity, $right_entity, $context);
 
   // Allow other modules to interact directly with the results.
-  drupal_alter('entity_diff', $diff, $context);
+  \Drupal::moduleHandler()->alter('entity_diff', $diff, $context);
 
   // We start off assuming all form elements are in the correct order.
   $diff['#sorted'] = TRUE;
 
   // Field rows. Recurse through all child elements.
   $count = 0;
-  foreach (element_children($diff) as $key) {
+  foreach (\Drupal\Core\Render\Element::children($diff) as $key) {
     if (!isset($diff[$key]['#states'])) {
       $diff[$key]['#states'] = array();
     }
@@ -642,7 +642,7 @@ function diff_inline_show($node, $vid = 0, $metadata = TRUE) {
     $old_node = node_load($node->nid, $old, TRUE);
     node_build_content($old_node);
     $old = drupal_render($old_node->content);
-    $output = $metadata ? theme('diff_inline_metadata', array('node' => $new_node)) : '';
+    $output = $metadata ? _theme('diff_inline_metadata', array('node' => $new_node)) : '';
     $output .= diff_get_inline($old, $new);
     return $output;
   }
diff --git a/diff.routing.yml b/diff.routing.yml
new file mode 100644
index 0000000..d4e377e
--- /dev/null
+++ b/diff.routing.yml
@@ -0,0 +1,51 @@
+diff.node_revisions_list:
+  path: '/node/{node}/revisions/list'
+  defaults:
+    _title: 'List revisions'
+    _content: '\Drupal\diff\Controller\DefaultController::diffsOverview'
+  requirements:
+    _custom_access: '\Drupal\diff\Controller\DefaultController::nodeRevisionAccess'
+diff.node_revisions_view:
+  path: '/node/{node}/revisions/view/{old_vid}/{new_vid}/{state}'
+  defaults:
+    _title: 'Compare revisions'
+    _content: '\Drupal\diff\Controller\DefaultController::diffsShow'
+  requirements:
+    _custom_access: '\Drupal\diff\Controller\DefaultController::nodeRevisionAccess'
+diff.node_revisions_view_latest:
+  path: '/node/{node}/revisions/view/latest'
+  defaults:
+    _title: 'Show latest difference'
+    _content: '\Drupal\diff\Controller\DefaultController::latest'
+  requirements:
+    _permission: 'access content'
+diff.admin_config_content_diff:
+  path: /admin/config/content/diff
+  defaults:
+    _title: Diff
+    _form: \Drupal\diff\Form\DiffAdminSettings
+  requirements:
+    _permission: 'administer site configuration'
+diff.admin_config_content_diff_fields:
+  path: '/admin/config/content/diff/fields/{type}'
+  defaults:
+    _title: 'Global field settings'
+    _form: \Drupal\diff\Form\DiffAdminGlobalFieldSettings
+  requirements:
+    _permission: 'administer site configuration'
+diff.admin_config_content_diff_entities:
+  path: /admin/config/content/diff/entities
+  defaults:
+    _title: Entities
+    entity_type: node
+    _form: \Drupal\diff\Form\DiffAdminGlobalEntitySettings
+  requirements:
+    _permission: 'administer site configuration'
+diff.admin_config_content_diff_entities_user:
+  path: /admin/config/content/diff/entities/user
+  defaults:
+    _title: Users
+    entity_type: user
+    _form: \Drupal\diff\Form\DiffAdminGlobalEntitySettings
+  requirements:
+    _permission: 'administer site configuration'
diff --git a/diff.theme.inc b/diff.theme.inc
index 23426ba..ad32965 100644
--- a/diff.theme.inc
+++ b/diff.theme.inc
@@ -19,7 +19,7 @@ function theme_diff_node_revisions($vars) {
     array('data' => t('Operations'), 'colspan' => 2),
   );
   if (isset($form['info']) && is_array($form['info'])) {
-    foreach (element_children($form['info']) as $key) {
+    foreach (\Drupal\Core\Render\Element::children($form['info']) as $key) {
       $row = array();
       if (isset($form['operations'][$key][0])) {
         // Note: even if the commands for revert and delete are not permitted,
@@ -60,7 +60,7 @@ function theme_diff_node_revisions($vars) {
       }
     }
   }
-  $output .= theme('table__diff__revisions', array(
+  $output .= _theme('table__diff__revisions', array(
     'header' => $header,
     'rows' => $rows,
     'sticky' => FALSE,
@@ -100,7 +100,7 @@ function theme_diff_empty_line($vars) {
  * Theme function for inline diff form.
  */
 function theme_diff_inline_form($vars) {
-  if ($theme = variable_get('diff_theme', 'default')) {
+  if ($theme = \Drupal::config('diff.settings')->get('diff_theme')) {
     drupal_add_css(drupal_get_path('module', 'diff') . "/css/diff.{$theme}.css");
   }
   return drupal_render_children($vars['form']);
@@ -110,7 +110,7 @@ function theme_diff_inline_form($vars) {
  * Display inline diff metadata.
  */
 function theme_diff_inline_metadata($vars) {
-  if ($theme = variable_get('diff_theme', 'default')) {
+  if ($theme = \Drupal::config('diff.settings')->get('diff_theme')) {
     drupal_add_css(drupal_get_path('module', 'diff') . "/css/diff.{$theme}.css");
   }
   $node = $vars['node'];
@@ -118,15 +118,15 @@ function theme_diff_inline_metadata($vars) {
   $output = "<div class='diff-inline-metadata clear-block'>";
   $output .= "<div class='diff-inline-byline'>";
   $output .= t('Updated by !name on @date', array(
-    '!name' => theme('username', array('account' => user_load($node->revision_uid))),
+    '!name' => _theme('username', array('account' => user_load($node->revision_uid))),
     '@date' => format_date($node->revision_timestamp, 'small'),
   ));
   $output .= "</div>";
   $output .= "<div class='diff-inline-legend clear-block'>";
   $output .= "<label>" . t('Legend') . "</label>";
-  $output .= theme('diff_inline_chunk', array('text' => t('Added'), 'type' => 'add'));
-  $output .= theme('diff_inline_chunk', array('text' => t('Changed'), 'type' => 'change'));
-  $output .= theme('diff_inline_chunk', array('text' => t('Deleted'), 'type' => 'delete'));
+  $output .= _theme('diff_inline_chunk', array('text' => t('Added'), 'type' => 'add'));
+  $output .= _theme('diff_inline_chunk', array('text' => t('Changed'), 'type' => 'change'));
+  $output .= _theme('diff_inline_chunk', array('text' => t('Deleted'), 'type' => 'delete'));
   $output .= "</div>";
   $output .= "</div>";
   return $output;
diff --git a/includes/node.inc b/includes/node.inc
index 6d33f63..664f38a 100644
--- a/includes/node.inc
+++ b/includes/node.inc
@@ -15,7 +15,7 @@ function _node_entity_diff_additional_options_title($old_node, $new_node, $conte
     '#states' => array(),
     '#weight' => -5,
     '#settings' => array(
-      'show_header' => variable_get('diff_show_header_node', 1),
+      'show_header' => \Drupal::config('diff.settings')->get('diff_show_header_node'),
     ),
   );
   foreach ($context['states'] as $state) {
@@ -176,7 +176,7 @@ function _node_entity_diff_additional_options_date_field($label, $old_date, $new
  * Private callback function to render the comment field.
  */
 function _node_entity_diff_additional_options_comment($old_node, $new_node, $context) {
-  if (!module_exists('comment')) {
+  if (!\Drupal::moduleHandler()->moduleExists('comment')) {
     return array();
   }
   $row = array(
diff --git a/includes/user.inc b/includes/user.inc
index 7ebd566..7864b26 100644
--- a/includes/user.inc
+++ b/includes/user.inc
@@ -17,15 +17,15 @@ function _user_entity_diff_additional_options_name($old_user, $new_user, $contex
     '#states' => array(),
     '#weight' => -5,
     '#settings' => array(
-      'show_header' => variable_get('diff_show_header_user', 1),
+      'show_header' => \Drupal::config('diff.settings')->get('diff_show_header_user'),
     ),
   );
   foreach ($context['states'] as $state) {
     switch ($state) {
       case 'rendered':
         $row['#states'][$state] = array(
-          '#old' => theme('username', array('account' => $old_user)),
-          '#new' => theme('username', array('account' => $old_user)),
+          '#old' => _theme('username', array('account' => $old_user)),
+          '#new' => _theme('username', array('account' => $old_user)),
         );
         break;
 
diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php
new file mode 100644
index 0000000..9e9c143
--- /dev/null
+++ b/src/Controller/DefaultController.php
@@ -0,0 +1,236 @@
+<?php /**
+ * @file
+ * Contains \Drupal\diff\Controller\DefaultController.
+ */
+
+namespace Drupal\diff\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+
+/**
+ * Default controller for the diff module.
+ */
+class DefaultController extends ControllerBase {
+
+/**
+ * Access callback for the node revisions page.
+ */
+function nodeRevisionAccess(\Drupal\node\NodeInterface $node, $op = 'view') {
+  $may_revision_this_type = \Drupal::config('diff.settings')->get('diff_enable_revisions_page_node_' . $node->bundle()) || \Drupal::currentUser()->hasPermission('administer nodes');
+  return $may_revision_this_type && _node_revision_access($node, $op);
+}
+
+/**
+ * Menu callback - an overview table of older revisions.
+ *
+ * Generate an overview table of older revisions of a node and provide
+ * an input form to select two revisions for a comparison.
+ */
+function diffsOverview(\Drupal\node\NodeInterface $node) {
+  drupal_set_title(t('Revisions for %title', array('%title' => $node->label())), PASS_THROUGH);
+  return drupal_get_form('diff_node_revisions', $node);
+}
+
+/**
+ * Create a comparison for the node between versions 'old_vid' and 'new_vid'.
+ *
+ * @param object $node
+ *   Node on which to perform comparison
+ * @param integer $old_vid
+ *   Version ID of the old revision.
+ * @param integer $new_vid
+ *   Version ID of the new revision.
+ */
+function diffsShow(\Drupal\node\NodeInterface $node, $old_vid, $new_vid, $state = NULL) {
+  // Attaches the CSS.
+  $build['#attached'] = diff_build_attachments();
+
+  $default_state = \Drupal::config('diff.settings')->get('diff_default_state_node');
+  if (empty($state)) {
+    $state = $default_state;
+  }
+  $state = str_replace('-', '_', $state);
+  if (!array_key_exists($state, diff_available_states())) {
+    $state = $default_state;
+  }
+
+  // Same title as the 'Revisions' tab. Blocked by non-page requests.
+  if (node_is_page($node)) {
+    drupal_set_title(t('Revisions for %title', array('%title' => $node->label())), PASS_THROUGH);
+  }
+  $node_revisions = node_revision_list($node);
+
+  $old_node = node_load($node->id(), $old_vid);
+  $new_node = node_load($node->id(), $new_vid);
+
+  // Generate table header (date, username, log message).
+  $old_header = t('!date by !username', array(
+    '!date' => l(format_date($old_node->revision_timestamp), "node/$node->nid/revisions/$old_node->vid/view", array('absolute' => 1)),
+    '!username' => _theme('username', array('account' => $node_revisions[$old_vid])),
+  ));
+  $new_header = t('!date by !username', array(
+    '!date' => l(format_date($new_node->revision_timestamp), "node/$node->nid/revisions/$new_node->vid/view", array('absolute' => 1)),
+    '!username' => _theme('username', array('account' => $node_revisions[$new_vid])),
+  ));
+
+  $old_log = $old_node->log != '' ? '<p class="revision-log">' . filter_xss($old_node->log) . '</p>' : '';
+  $new_log = $new_node->log != '' ? '<p class="revision-log">' . filter_xss($new_node->log) . '</p>' : '';
+
+  // Generate previous diff/next diff links.
+  $nav_suffix = ($default_state != $state) ? '/' . str_replace('_', '-', $state) : '';
+  $next_vid = _diff_get_next_vid($node_revisions, $new_vid);
+  if ($next_vid) {
+    $next_link = l(t('Next difference >'), 'node/' . $node->id() . '/revisions/view/' . $new_vid . '/' . $next_vid . $nav_suffix, array('absolute' => 1));
+  }
+  else {
+    $next_link = '';
+  }
+  $prev_vid = _diff_get_previous_vid($node_revisions, $old_vid);
+  if ($prev_vid) {
+    $prev_link = l(t('< Previous difference'), 'node/' . $node->id() . '/revisions/view/' . $prev_vid . '/' . $old_vid . $nav_suffix, array('absolute' => 1));
+  }
+  else {
+    $prev_link = '';
+  }
+
+  $header = _diff_default_header($old_header, $new_header);
+  $rows = array();
+  if ($old_log || $new_log) {
+    $rows['logs'] = array(
+      array(
+        'data' => $old_log,
+        'colspan' => 2,
+      ),
+      array(
+        'data' => $new_log,
+        'colspan' => 2,
+      ),
+    );
+  }
+  $rows['navigation'] = array(
+    array(
+      'data' => $prev_link,
+      'class' => array('diff-prevlink'),
+      'colspan' => 2,
+    ),
+    array(
+      'data' => $next_link,
+      'class' => array('diff-nextlink'),
+      'colspan' => 2,
+    ),
+  );
+
+  $links = array();
+  foreach (diff_available_states('node') as $alternative_state => $label) {
+    if ($alternative_state == $state) {
+      // @todo: Should we show both or just alternatives?
+    }
+    $links[$alternative_state] = array(
+      'title' => $label,
+      'href' => "node/{$node->id()}/revisions/view/{$old_vid}/{$new_vid}" . ($alternative_state == $default_state ? '' : '/' . str_replace('_', '-', $alternative_state)),
+    );
+  }
+  if (count($links) > 1) {
+    $state_links = _theme('links', array(
+      'links' => $links,
+      'attributes' => array('class' => array('links', 'inline')),
+    ));
+    $rows['states'] = array(
+      array(
+        'data' => $state_links,
+        'class' => 'diff-links',
+        'colspan' => 4,
+      ),
+    );
+  }
+  $rows = array_merge($rows, _diff_body_rows($old_node, $new_node, $state));
+
+  $build['diff_table'] = array(
+    '#theme' => 'table__diff__standard',
+    '#header' => $header,
+    '#rows' => $rows,
+    '#attributes' => array('class' => array('diff')),
+    '#colgroups' => _diff_default_cols(),
+    '#sticky' => FALSE,
+  );
+
+  // Allow users to hide or set the display mode of the preview.
+  if (node_is_page($node) && $view_mode = \Drupal::config('diff.settings')->get('diff_view_mode_preview_node_' . $new_node->type)) {
+    $header = '';
+    if ($node->vid == $new_vid) {
+      $header .= '<div class="diff-section-title">' . t('Current revision:') . '</div>';
+    }
+    else {
+      $header .= '<div class="diff-section-title">' . t('Revision of @new_date:', array('@new_date' => format_date($new_node->revision_timestamp))) . '</div>';
+    }
+    $build['diff_preview']['header']['#markup'] = $header;
+    // Don't include node links or comments when viewing the diff.
+    $build['diff_preview']['content'] = node_view($new_node, $view_mode);
+    if (isset($build['diff_preview']['content']['links'])) {
+      unset($build['diff_preview']['content']['links']);
+    }
+    if (isset($build['diff_preview']['content']['comments'])) {
+      unset($build['diff_preview']['content']['comments']);
+    }
+  }
+  return $build;
+}
+
+/**
+ * Menu callback - show latest diff for a given node.
+ */
+function latest(\Drupal\node\NodeInterface $node) {
+  $revisions = node_revision_list($node);
+  if (count($revisions) < 2 || !diff_node_revision_access($node, 'view')) {
+    drupal_goto('node/' . $node->id());
+  }
+  $new = array_shift($revisions);
+  $old = array_shift($revisions);
+  drupal_goto("node/{$node->id()}/revisions/view/{$old->vid}/{$new->vid}");
+}
+
+/**
+ * Menu callback - provides an overview of Diff support and global settings.
+ */
+function adminFieldOverview() {
+  $build['info'] = array(
+    '#markup' => '<p>' . t('This table provides a summary of the field type support found on the system. It is recommended that you use global settings whenever possible to configure field comparison settings.') . '</p>',
+  );
+
+  $header = array(t('Type'), t('Module'), t('Operations'));
+  $rows = array();
+
+  // Skip field types which have no widget types.
+  $field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
+  $widgets = array();
+  foreach (field_info_widget_types() as $name => $widget_type) {
+    foreach ($widget_type['field types'] as $widget_field_type) {
+      if (isset($field_types[$widget_field_type])) {
+        $widgets[$widget_field_type][$name] = $widget_type['label'];
+      }
+    }
+  }
+
+  foreach ($field_types as $field_name => $field_type) {
+    if (!empty($widgets[$field_name])) {
+      $row = array();
+      $row[] = t('@field_label (%field_type)', array(
+        '@field_label' => $field_type['label'],
+        '%field_type' => $field_name,
+      ));
+      $row[] = $field_type['module'];
+      $row[] = l(t('Global settings'), 'admin/config/content/diff/fields/' . $field_name);
+      $rows[] = $row;
+    }
+  }
+
+  $build['category_table'] = array(
+    '#theme' => 'table',
+    '#header' => $header,
+    '#rows' => $rows,
+    '#empty' => t('The system has no configurable fields.'),
+  );
+  return $build;
+}
+
+}
diff --git a/src/Diff.php b/src/Diff.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/Diff.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/DiffFormatter.php b/src/DiffFormatter.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/DiffFormatter.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/DrupalDiffFormatter.php b/src/DrupalDiffFormatter.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/DrupalDiffFormatter.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/DrupalDiffInline.php b/src/DrupalDiffInline.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/DrupalDiffInline.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/Form/DiffAdminGlobalEntitySettings.php b/src/Form/DiffAdminGlobalEntitySettings.php
new file mode 100644
index 0000000..d8ea8d0
--- /dev/null
+++ b/src/Form/DiffAdminGlobalEntitySettings.php
@@ -0,0 +1,92 @@
+<?php /**
+ * @file
+ * Contains \Drupal\diff\Form\DiffAdminGlobalEntitySettings.
+ */
+
+namespace Drupal\diff\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+
+class DiffAdminGlobalEntitySettings extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'diff_admin_global_entity_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('diff.settings');
+
+    foreach (Element::children($form) as $variable) {
+      $config->set($variable, $form_state->getValue($form[$variable]['#parents']));
+    }
+    $config->save();
+
+    if (method_exists($this, '_submitForm')) {
+      $this->_submitForm($form, $form_state);
+    }
+
+    parent::submitForm($form, $form_state);
+  }
+
+/**
+ * Global entity settings.
+ */
+function buildForm(array $form, FormStateInterface $form_state, $entity_type = NULL) {
+  $entity_info = \Drupal::entityManager()->getDefinition($entity_type);
+  drupal_set_title(t('Diff settings for %entity_label entities', array('%entity_label' => $entity_info['label'])), PASS_THROUGH);
+
+  if ($options = module_invoke_all('entity_diff_options', $entity_type)) {
+    $form['diff_additional_options_' . $entity_type] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Property options'),
+      '#default_value' => \Drupal::config('diff.settings')->get('diff_additional_options_' . $entity_type),
+      '#options' => $options,
+    );
+  }
+  else {
+    $form['diff_additional_options_' . $entity_type] = array(
+      '#type' => 'value',
+      '#value' => array(),
+    );
+  }
+
+  $form['diff_show_header_' . $entity_type] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Show entity label row header'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_show_header_' . $entity_type),
+  );
+  if (\Drupal::currentUser()->hasPermission('administer permissions')) {
+    $admin_link = l(t('View the administration theme'), 'admin/people/permissions', array('fragment' => 'edit-view-the-administration-theme'));
+  }
+  else {
+    $admin_link = t('View the administration theme');
+  }
+  $form['diff_admin_path_' . $entity_type] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Use administration theme'),
+    '#description' => t('This option will enable users with the <em>!link</em> permission to use the admin theme when doing comparisons.', array('!link' => $admin_link)),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_admin_path_' . $entity_type),
+  );
+  $form['diff_default_state_' . $entity_type] = array(
+    '#type' => 'select',
+    '#title' => t('Diff default state'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_default_state_' . $entity_type),
+    '#options' => array(
+      'raw' => t('HTML view'),
+      'raw_plain' => t('Plain view'),
+    ),
+    '#empty_option' => t('- None -'),
+    '#description' => t('Default display to show when viewing a diff, html tags in diffed result or as plain text.'),
+  );
+  return parent::buildForm($form, $form_state);
+}
+
+}
diff --git a/src/Form/DiffAdminGlobalFieldSettings.php b/src/Form/DiffAdminGlobalFieldSettings.php
new file mode 100644
index 0000000..291f798
--- /dev/null
+++ b/src/Form/DiffAdminGlobalFieldSettings.php
@@ -0,0 +1,70 @@
+<?php /**
+ * @file
+ * Contains \Drupal\diff\Form\DiffAdminGlobalFieldSettings.
+ */
+
+namespace Drupal\diff\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+
+class DiffAdminGlobalFieldSettings extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'diff_admin_global_field_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('diff.settings');
+
+    foreach (Element::children($form) as $variable) {
+      $config->set($variable, $form_state->getValue($form[$variable]['#parents']));
+    }
+    $config->save();
+
+    if (method_exists($this, '_submitForm')) {
+      $this->_submitForm($form, $form_state);
+    }
+
+    parent::submitForm($form, $form_state);
+  }
+
+/**
+ * Menu form callback for the field settings.
+ */
+function buildForm(array $form, FormStateInterface $form_state, $type = NULL) {
+  module_load_include('diff.inc', 'diff');
+
+  $field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
+  if (!isset($field_types[$type])) {
+    drupal_set_message(t('Invalid field type.'), 'error');
+    drupal_goto('admin/config/content/diff/fields');
+  }
+  $field_type = $field_types[$type];
+
+  // Set the title to give more context to this page.
+  drupal_set_title(t('Global settings for %label fields', array(
+    '%label' => $field_type['label'],
+  )), PASS_THROUGH);
+
+  $variable_name = "diff_{$field_type['module']}_field_{$type}_default_options";
+  $settings = \Drupal::config('diff.settings')->get($variable_name);
+  $settings = _diff_field_default_settings($field_type['module'], $type, $settings);
+  $func = $field_type['module'] . '_field_diff_options_form';
+  if (function_exists($func) && ($options_form = $func($type, $settings))) {
+    $form[$variable_name] = $options_form;
+  }
+  $form[$variable_name]['#tree'] = TRUE;
+
+  diff_global_settings_form($form[$variable_name], $form_state, $type, $settings);
+  return parent::buildForm($form, $form_state);
+}
+
+}
diff --git a/src/Form/DiffAdminSettings.php b/src/Form/DiffAdminSettings.php
new file mode 100644
index 0000000..7052c9d
--- /dev/null
+++ b/src/Form/DiffAdminSettings.php
@@ -0,0 +1,85 @@
+<?php /**
+ * @file
+ * Contains \Drupal\diff\Form\DiffAdminSettings.
+ */
+
+namespace Drupal\diff\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+
+class DiffAdminSettings extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'diff_admin_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('diff.settings');
+
+    foreach (Element::children($form) as $variable) {
+      $config->set($variable, $form_state->getValue($form[$variable]['#parents']));
+    }
+    $config->save();
+
+    if (method_exists($this, '_submitForm')) {
+      $this->_submitForm($form, $form_state);
+    }
+
+    parent::submitForm($form, $form_state);
+  }
+
+/**
+ * General configuration form for controlling the diff behaviour.
+ */
+function buildForm(array $form, FormStateInterface $form_state) {
+  $form['diff_theme'] = array(
+    '#type' => 'select',
+    '#title' => t('CSS options'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_theme'),
+    '#options' => array(
+      'default' => t('Classic'),
+      'boxes' => t('Boxes'),
+    ),
+    '#empty_option' => t('- None -'),
+    '#description' => t('Alter the CSS used when displaying diff results.'),
+  );
+  $form['diff_radio_behavior'] = array(
+    '#type' => 'select',
+    '#title' => t('Diff radio behavior'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_radio_behavior'),
+    '#options' => array(
+      'simple' => t('Simple exclusion'),
+      'linear' => t('Linear restrictions'),
+    ),
+    '#empty_option' => t('- None -'),
+    '#description' => t('<em>Simple exclusion</em> means that users will not be able to select the same revision, <em>Linear restrictions</em> means that users can only select older or newer revisions of the current selections.'),
+  );
+
+  $options = array_combine(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10), array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+  $form['diff_context_lines_leading'] = array(
+    '#type' => 'select',
+    '#title' => t('Leading context lines'),
+    '#description' => t('This governs the number of unchanged leading context "lines" to preserve.'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_context_lines_leading'),
+    '#options' => $options,
+  );
+  $form['diff_context_lines_trailing'] = array(
+    '#type' => 'select',
+    '#title' => t('Trailing context lines'),
+    '#description' => t('This governs the number of unchanged trailing context "lines" to preserve.'),
+    '#default_value' => \Drupal::config('diff.settings')->get('diff_context_lines_trailing'),
+    '#options' => $options,
+  );
+
+  return parent::buildForm($form, $form_state);
+}
+
+}
diff --git a/src/MappedDiff.php b/src/MappedDiff.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/MappedDiff.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/Plugin/Block/Inline.php b/src/Plugin/Block/Inline.php
new file mode 100644
index 0000000..46398fd
--- /dev/null
+++ b/src/Plugin/Block/Inline.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\diff\Plugin\Block\Inline.
+ */
+
+namespace Drupal\diff\Plugin\Block;
+
+use Drupal\Core\Block\BlockBase;
+
+/**
+ * Provides the Inline block.
+ *
+ * @Block(
+ *   id = "inline",
+ *   admin_label = @Translation("Inline differences")
+ * )
+ */
+class Inline extends BlockBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build() {
+    /**
+     * @FIXME
+     * hook_block_view() has been removed in Drupal 8. You should move your
+     * block's code into this method and delete diff_block_view()
+     * as soon as possible!
+     */
+    return diff_block_view('inline');
+  }
+
+}
diff --git a/src/WordLevelDiff.php b/src/WordLevelDiff.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/WordLevelDiff.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_DiffEngine.php b/src/_DiffEngine.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_DiffEngine.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_DiffOp.php b/src/_DiffOp.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_DiffOp.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_DiffOp_Add.php b/src/_DiffOp_Add.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_DiffOp_Add.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_DiffOp_Change.php b/src/_DiffOp_Change.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_DiffOp_Change.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_DiffOp_Copy.php b/src/_DiffOp_Copy.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_DiffOp_Copy.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_DiffOp_Delete.php b/src/_DiffOp_Delete.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_DiffOp_Delete.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
diff --git a/src/_HWLDF_WordAccumulator.php b/src/_HWLDF_WordAccumulator.php
new file mode 100644
index 0000000..f0c0d51
--- /dev/null
+++ b/src/_HWLDF_WordAccumulator.php
@@ -0,0 +1,1355 @@
+<?php
+
+/**
+ * @file
+ * A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+ *
+ * Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+ * You may copy this code freely under the conditions of the GPL.
+ */
+
+define('USE_ASSERTS', FALSE);
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp {
+  var $type;
+  var $orig;
+  var $closing;
+
+  function reverse() {
+    trigger_error('pure virtual', E_USER_ERROR);
+  }
+
+  function norig() {
+    return $this->orig ? sizeof($this->orig) : 0;
+  }
+
+  function nclosing() {
+    return $this->closing ? sizeof($this->closing) : 0;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+  var $type = 'copy';
+
+  function _DiffOp_Copy($orig, $closing = FALSE) {
+    if (!is_array($closing)) {
+      $closing = $orig;
+    }
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Copy($this->closing, $this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+  var $type = 'delete';
+
+  function _DiffOp_Delete($lines) {
+    $this->orig = $lines;
+    $this->closing = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Add($this->orig);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+  var $type = 'add';
+
+  function _DiffOp_Add($lines) {
+    $this->closing = $lines;
+    $this->orig = FALSE;
+  }
+
+  function reverse() {
+    return new _DiffOp_Delete($this->closing);
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+  var $type = 'change';
+
+  function _DiffOp_Change($orig, $closing) {
+    $this->orig = $orig;
+    $this->closing = $closing;
+  }
+
+  function reverse() {
+    return new _DiffOp_Change($this->closing, $this->orig);
+  }
+}
+
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *   http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *   http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *   ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _DiffEngine {
+  function MAX_XREF_LENGTH() {
+    return 10000;
+  }
+
+  function diff($from_lines, $to_lines) {
+
+    $n_from = sizeof($from_lines);
+    $n_to = sizeof($to_lines);
+
+    $this->xchanged = $this->ychanged = array();
+    $this->xv = $this->yv = array();
+    $this->xind = $this->yind = array();
+    unset($this->seq);
+    unset($this->in_seq);
+    unset($this->lcs);
+
+    // Skip leading common lines.
+    for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+      if ($from_lines[$skip] !== $to_lines[$skip]) {
+        break;
+      }
+      $this->xchanged[$skip] = $this->ychanged[$skip] = FALSE;
+    }
+    // Skip trailing common lines.
+    $xi = $n_from;
+    $yi = $n_to;
+    for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+      if ($from_lines[$xi] !== $to_lines[$yi]) {
+        break;
+      }
+      $this->xchanged[$xi] = $this->ychanged[$yi] = FALSE;
+    }
+
+    // Ignore lines which do not exist in both files.
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+    }
+
+    for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+      $line = $to_lines[$yi];
+      if ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $yhash[$this->_line_hash($line)] = 1;
+      $this->yv[] = $line;
+      $this->yind[] = $yi;
+    }
+    for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+      $line = $from_lines[$xi];
+      if ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) {
+        continue;
+      }
+      $this->xv[] = $line;
+      $this->xind[] = $xi;
+    }
+
+    // Find the LCS.
+    $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+
+    // Merge edits when possible
+    $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+    $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+    // Compute the edit operations.
+    $edits = array();
+    $xi = $yi = 0;
+    while ($xi < $n_from || $yi < $n_to) {
+      USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+      USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+      // Skip matching "snake".
+      $copy = array();
+      while ( $xi < $n_from && $yi < $n_to && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+        $copy[] = $from_lines[$xi++];
+        ++$yi;
+      }
+      if ($copy) {
+        $edits[] = new _DiffOp_Copy($copy);
+      }
+      // Find deletes & adds.
+      $delete = array();
+      while ($xi < $n_from && $this->xchanged[$xi]) {
+        $_fl = $from_lines[$xi++];
+        if (strlen($_fl)) {
+          $delete[] = $_fl;
+        }
+      }
+      $add = array();
+      while ($yi < $n_to && $this->ychanged[$yi]) {
+        $_tl = $to_lines[$yi++];
+        if (strlen($_tl)) {
+          $add[] = $_tl;
+        }
+      }
+      if ($delete && $add) {
+        $edits[] = new _DiffOp_Change($delete, $add);
+      }
+      elseif ($delete) {
+        $edits[] = new _DiffOp_Delete($delete);
+      }
+      elseif ($add) {
+        $edits[] = new _DiffOp_Add($add);
+      }
+    }
+    return $edits;
+  }
+
+  /**
+   * Returns the whole line if it's small enough, or the MD5 hash otherwise.
+   */
+  function _line_hash($line) {
+    if (drupal_strlen($line) > $this->MAX_XREF_LENGTH()) {
+      return md5($line);
+    }
+    else {
+      return $line;
+    }
+  }
+
+
+  /**
+   * Divide the Largest Common Subsequence (LCS) of the sequences
+   * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+   * sized segments.
+   *
+   * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+   * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+   * sub sequences.  The first sub-sequence is contained in [X0, X1),
+   * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+   * that (X0, Y0) == (XOFF, YOFF) and
+   * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+   *
+   * This function assumes that the first lines of the specified portions
+   * of the two files do not match, and likewise that the last lines do not
+   * match.  The caller must trim matching lines from the beginning and end
+   * of the portions it is going to specify.
+   */
+  function _diag($xoff, $xlim, $yoff, $ylim, $nchunks) {
+    $flip = FALSE;
+
+    if ($xlim - $xoff > $ylim - $yoff) {
+      // Things seems faster (I'm not sure I understand why)
+      // when the shortest sequence in X.
+      $flip = TRUE;
+      list($xoff, $xlim, $yoff, $ylim) = array($yoff, $ylim, $xoff, $xlim);
+    }
+
+    if ($flip) {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->xv[$i]][] = $i;
+      }
+    }
+    else {
+      for ($i = $ylim - 1; $i >= $yoff; $i--) {
+        $ymatches[$this->yv[$i]][] = $i;
+      }
+    }
+    $this->lcs = 0;
+    $this->seq[0]= $yoff - 1;
+    $this->in_seq = array();
+    $ymids[0] = array();
+
+    $numer = $xlim - $xoff + $nchunks - 1;
+    $x = $xoff;
+    for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+      if ($chunk > 0) {
+        for ($i = 0; $i <= $this->lcs; $i++) {
+          $ymids[$i][$chunk-1] = $this->seq[$i];
+        }
+      }
+
+      $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+      for ( ; $x < $x1; $x++) {
+        $line = $flip ? $this->yv[$x] : $this->xv[$x];
+        if (empty($ymatches[$line])) {
+          continue;
+        }
+        $matches = $ymatches[$line];
+        reset($matches);
+        while (list ($junk, $y) = each($matches)) {
+          if (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+            break;
+          }
+        }
+        while (list ($junk, $y) = each($matches)) {
+          if ($y > $this->seq[$k-1]) {
+            USE_ASSERTS && assert($y < $this->seq[$k]);
+            // Optimization: this is a common case:
+            // next match is just replacing previous match.
+            $this->in_seq[$this->seq[$k]] = FALSE;
+            $this->seq[$k] = $y;
+            $this->in_seq[$y] = 1;
+          }
+          elseif (empty($this->in_seq[$y])) {
+            $k = $this->_lcs_pos($y);
+            USE_ASSERTS && assert($k > 0);
+            $ymids[$k] = $ymids[$k-1];
+          }
+        }
+      }
+    }
+
+    $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+    $ymid = $ymids[$this->lcs];
+    for ($n = 0; $n < $nchunks - 1; $n++) {
+      $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+      $y1 = $ymid[$n] + 1;
+      $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+    }
+    $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+    return array($this->lcs, $seps);
+  }
+
+  function _lcs_pos($ypos) {
+
+    $end = $this->lcs;
+    if ($end == 0 || $ypos > $this->seq[$end]) {
+      $this->seq[++$this->lcs] = $ypos;
+      $this->in_seq[$ypos] = 1;
+      return $this->lcs;
+    }
+
+    $beg = 1;
+    while ($beg < $end) {
+      $mid = (int)(($beg + $end) / 2);
+      if ($ypos > $this->seq[$mid]) {
+        $beg = $mid + 1;
+      }
+      else {
+        $end = $mid;
+      }
+    }
+
+    USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+    $this->in_seq[$this->seq[$end]] = FALSE;
+    $this->seq[$end] = $ypos;
+    $this->in_seq[$ypos] = 1;
+    return $end;
+  }
+
+  /**
+   * Find LCS of two sequences.
+   *
+   * The results are recorded in the vectors $this->{x,y}changed[], by
+   * storing a 1 in the element for each line that is an insertion
+   * or deletion (ie. is not in the LCS).
+   *
+   * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+   *
+   * Note that XLIM, YLIM are exclusive bounds.
+   * All line numbers are origin-0 and discarded lines are not counted.
+   */
+  function _compareseq($xoff, $xlim, $yoff, $ylim) {
+
+    // Slide down the bottom initial diagonal.
+    while ($xoff < $xlim && $yoff < $ylim && $this->xv[$xoff] == $this->yv[$yoff]) {
+      ++$xoff;
+      ++$yoff;
+    }
+
+    // Slide up the top initial diagonal.
+    while ($xlim > $xoff && $ylim > $yoff && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+      --$xlim;
+      --$ylim;
+    }
+
+    if ($xoff == $xlim || $yoff == $ylim) {
+      $lcs = 0;
+    }
+    else {
+      // This is ad hoc but seems to work well.
+      //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+      //$nchunks = max(2, min(8, (int)$nchunks));
+      $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+      list($lcs, $seps)
+      = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
+    }
+
+    if ($lcs == 0) {
+      // X and Y sequences have no common subsequence:
+      // mark all changed.
+      while ($yoff < $ylim) {
+        $this->ychanged[$this->yind[$yoff++]] = 1;
+      }
+      while ($xoff < $xlim) {
+        $this->xchanged[$this->xind[$xoff++]] = 1;
+      }
+    }
+    else {
+      // Use the partitions to split this problem into subproblems.
+      reset($seps);
+      $pt1 = $seps[0];
+      while ($pt2 = next($seps)) {
+        $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+        $pt1 = $pt2;
+      }
+    }
+  }
+
+  /**
+   * Adjust inserts/deletes of identical lines to join changes
+   * as much as possible.
+   *
+   * We do something when a run of changed lines include a
+   * line at one end and has an excluded, identical line at the other.
+   * We are free to choose which identical line is included.
+   * `compareseq' usually chooses the one at the beginning,
+   * but usually it is cleaner to consider the following identical line
+   * to be the "change".
+   *
+   * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+   */
+  function _shift_boundaries($lines, &$changed, $other_changed) {
+    $i = 0;
+    $j = 0;
+
+    USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+    $len = sizeof($lines);
+    $other_len = sizeof($other_changed);
+
+    while (1) {
+      /*
+       * Scan forwards to find beginning of another run of changes.
+       * Also keep track of the corresponding point in the other file.
+       *
+       * Throughout this code, $i and $j are adjusted together so that
+       * the first $i elements of $changed and the first $j elements
+       * of $other_changed both contain the same number of zeros
+       * (unchanged lines).
+       * Furthermore, $j is always kept so that $j == $other_len or
+       * $other_changed[$j] == FALSE.
+       */
+      while ($j < $other_len && $other_changed[$j]) {
+        $j++;
+      }
+      while ($i < $len && ! $changed[$i]) {
+        USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+        $i++;
+        $j++;
+        while ($j < $other_len && $other_changed[$j]) {
+          $j++;
+        }
+      }
+
+      if ($i == $len) {
+        break;
+      }
+      $start = $i;
+
+      // Find the end of this run of changes.
+      while (++$i < $len && $changed[$i]) {
+        continue;
+      }
+
+      do {
+        /*
+         * Record the length of this run of changes, so that
+         * we can later determine whether the run has grown.
+         */
+        $runlength = $i - $start;
+
+        /*
+         * Move the changed region back, so long as the
+         * previous unchanged line matches the last changed one.
+         * This merges with previous changed regions.
+         */
+        while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+          $changed[--$start] = 1;
+          $changed[--$i] = FALSE;
+          while ($start > 0 && $changed[$start - 1]) {
+            $start--;
+          }
+          USE_ASSERTS && assert('$j > 0');
+          while ($other_changed[--$j]) {
+            continue;
+          }
+          USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+        }
+
+        /*
+         * Set CORRESPONDING to the end of the changed run, at the last
+         * point where it corresponds to a changed run in the other file.
+         * CORRESPONDING == LEN means no such point has been found.
+         */
+        $corresponding = $j < $other_len ? $i : $len;
+
+        /*
+         * Move the changed region forward, so long as the
+         * first changed line matches the following unchanged one.
+         * This merges with following changed regions.
+         * Do this second, so that if there are no merges,
+         * the changed region is moved forward as far as possible.
+         */
+        while ($i < $len && $lines[$start] == $lines[$i]) {
+          $changed[$start++] = FALSE;
+          $changed[$i++] = 1;
+          while ($i < $len && $changed[$i]) {
+            $i++;
+          }
+          USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+          $j++;
+          if ($j < $other_len && $other_changed[$j]) {
+            $corresponding = $i;
+            while ($j < $other_len && $other_changed[$j]) {
+              $j++;
+            }
+          }
+        }
+      } while ($runlength != $i - $start);
+
+      /*
+       * If possible, move the fully-merged run of changes
+       * back to a corresponding run in the other file.
+       */
+      while ($corresponding < $i) {
+        $changed[--$start] = 1;
+        $changed[--$i] = 0;
+        USE_ASSERTS && assert('$j > 0');
+        while ($other_changed[--$j]) {
+          continue;
+        }
+        USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+      }
+    }
+  }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class Diff {
+  var $edits;
+
+  /**
+   * Constructor.
+   * Computes diff between sequences of strings.
+   *
+   * @param $from_lines array An array of strings.
+   *      (Typically these are lines from a file.)
+   * @param $to_lines array An array of strings.
+   */
+  function Diff($from_lines, $to_lines) {
+    $eng = new _DiffEngine;
+    $this->edits = $eng->diff($from_lines, $to_lines);
+    //$this->_check($from_lines, $to_lines);
+  }
+
+  /**
+   * Compute reversed Diff.
+   *
+   * SYNOPSIS:
+   *
+   *  $diff = new Diff($lines1, $lines2);
+   *  $rev = $diff->reverse();
+   * @return object A Diff object representing the inverse of the
+   *          original diff.
+   */
+  function reverse() {
+    $rev = $this;
+    $rev->edits = array();
+    foreach ($this->edits as $edit) {
+      $rev->edits[] = $edit->reverse();
+    }
+    return $rev;
+  }
+
+  /**
+   * Check for empty diff.
+   *
+   * @return bool True iff two sequences were identical.
+   */
+  function isEmpty() {
+    foreach ($this->edits as $edit) {
+      if ($edit->type != 'copy') {
+        return FALSE;
+      }
+    }
+    return TRUE;
+  }
+
+  /**
+   * Compute the length of the Longest Common Subsequence (LCS).
+   *
+   * This is mostly for diagnostic purposed.
+   *
+   * @return int The length of the LCS.
+   */
+  function lcs() {
+    $lcs = 0;
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $lcs += sizeof($edit->orig);
+      }
+    }
+    return $lcs;
+  }
+
+  /**
+   * Get the original set of lines.
+   *
+   * This reconstructs the $from_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The original sequence of strings.
+   */
+  function orig() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->orig) {
+        array_splice($lines, sizeof($lines), 0, $edit->orig);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Get the closing set of lines.
+   *
+   * This reconstructs the $to_lines parameter passed to the
+   * constructor.
+   *
+   * @return array The sequence of strings.
+   */
+  function closing() {
+    $lines = array();
+
+    foreach ($this->edits as $edit) {
+      if ($edit->closing) {
+        array_splice($lines, sizeof($lines), 0, $edit->closing);
+      }
+    }
+    return $lines;
+  }
+
+  /**
+   * Check a Diff for validity.
+   *
+   * This is here only for debugging purposes.
+   */
+  function _check($from_lines, $to_lines) {
+    if (serialize($from_lines) != serialize($this->orig())) {
+      trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($to_lines) != serialize($this->closing())) {
+      trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+    }
+
+    $rev = $this->reverse();
+    if (serialize($to_lines) != serialize($rev->orig())) {
+      trigger_error("Reversed original doesn't match", E_USER_ERROR);
+    }
+    if (serialize($from_lines) != serialize($rev->closing())) {
+      trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+    }
+
+
+    $prevtype = 'none';
+    foreach ($this->edits as $edit) {
+      if ( $prevtype == $edit->type ) {
+        trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+      }
+      $prevtype = $edit->type;
+    }
+
+    $lcs = $this->lcs();
+    trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
+  }
+}
+
+/**
+ * FIXME: bad name.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class MappedDiff extends Diff {
+  /**
+   * Constructor.
+   *
+   * Computes diff between sequences of strings.
+   *
+   * This can be used to compute things like
+   * case-insensitve diffs, or diffs which ignore
+   * changes in white-space.
+   *
+   * @param $from_lines array An array of strings.
+   *  (Typically these are lines from a file.)
+   *
+   * @param $to_lines array An array of strings.
+   *
+   * @param $mapped_from_lines array This array should
+   *  have the same size number of elements as $from_lines.
+   *  The elements in $mapped_from_lines and
+   *  $mapped_to_lines are what is actually compared
+   *  when computing the diff.
+   *
+   * @param $mapped_to_lines array This array should
+   *  have the same number of elements as $to_lines.
+   */
+  function MappedDiff($from_lines, $to_lines, $mapped_from_lines, $mapped_to_lines) {
+
+    assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+    assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+    $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+    $xi = $yi = 0;
+    for ($i = 0; $i < sizeof($this->edits); $i++) {
+      $orig = &$this->edits[$i]->orig;
+      if (is_array($orig)) {
+        $orig = array_slice($from_lines, $xi, sizeof($orig));
+        $xi += sizeof($orig);
+      }
+
+      $closing = &$this->edits[$i]->closing;
+      if (is_array($closing)) {
+        $closing = array_slice($to_lines, $yi, sizeof($closing));
+        $yi += sizeof($closing);
+      }
+    }
+  }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DiffFormatter {
+  /**
+   * Should a block header be shown?
+   */
+  var $show_header = TRUE;
+
+  /**
+   * Number of leading context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $leading_context_lines = 0;
+
+  /**
+   * Number of trailing context "lines" to preserve.
+   *
+   * This should be left at zero for this class, but subclasses
+   * may want to set this to other values.
+   */
+  var $trailing_context_lines = 0;
+
+  /**
+   * Format a diff.
+   *
+   * @param $diff object A Diff object.
+   * @return string The formatted output.
+   */
+  function format($diff) {
+    $xi = $yi = 1;
+    $block = FALSE;
+    $context = array();
+
+    $nlead = $this->leading_context_lines;
+    $ntrail = $this->trailing_context_lines;
+
+    $this->_start_diff();
+
+    foreach ($diff->edits as $edit) {
+      if ($edit->type == 'copy') {
+        if (is_array($block)) {
+          if (sizeof($edit->orig) <= $nlead + $ntrail) {
+            $block[] = $edit;
+          }
+          else {
+            if ($ntrail) {
+              $context = array_slice($edit->orig, 0, $ntrail);
+              $block[] = new _DiffOp_Copy($context);
+            }
+            $this->_block($x0, $ntrail + $xi - $x0, $y0, $ntrail + $yi - $y0, $block);
+            $block = FALSE;
+          }
+        }
+        $context = $edit->orig;
+      }
+      else {
+        if (! is_array($block)) {
+          $context = array_slice($context, sizeof($context) - $nlead);
+          $x0 = $xi - sizeof($context);
+          $y0 = $yi - sizeof($context);
+          $block = array();
+          if ($context) {
+            $block[] = new _DiffOp_Copy($context);
+          }
+        }
+        $block[] = $edit;
+      }
+
+      if ($edit->orig) {
+        $xi += sizeof($edit->orig);
+      }
+      if ($edit->closing) {
+        $yi += sizeof($edit->closing);
+      }
+    }
+
+    if (is_array($block)) {
+      $this->_block($x0, $xi - $x0, $y0, $yi - $y0, $block);
+    }
+    $end = $this->_end_diff();
+
+    if (!empty($xi)) {
+      $this->line_stats['counter']['x'] += $xi;
+    }
+    if (!empty($yi)) {
+      $this->line_stats['counter']['y'] += $yi;
+    }
+
+    return $end;
+  }
+
+  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+    $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+    foreach ($edits as $edit) {
+      if ($edit->type == 'copy') {
+        $this->_context($edit->orig);
+      }
+      elseif ($edit->type == 'add') {
+        $this->_added($edit->closing);
+      }
+      elseif ($edit->type == 'delete') {
+        $this->_deleted($edit->orig);
+      }
+      elseif ($edit->type == 'change') {
+        $this->_changed($edit->orig, $edit->closing);
+      }
+      else {
+        trigger_error('Unknown edit type', E_USER_ERROR);
+      }
+    }
+    $this->_end_block();
+  }
+
+  function _start_diff() {
+    ob_start();
+  }
+
+  function _end_diff() {
+    $val = ob_get_contents();
+    ob_end_clean();
+    return $val;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    if ($xlen > 1) {
+      $xbeg .= "," . ($xbeg + $xlen - 1);
+    }
+    if ($ylen > 1) {
+      $ybeg .= "," . ($ybeg + $ylen - 1);
+    }
+
+    return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      echo $header . "\n";
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix = ' ') {
+    foreach ($lines as $line) {
+      echo "$prefix $line\n";
+    }
+  }
+
+  function _context($lines) {
+    $this->_lines($lines);
+  }
+
+  function _added($lines) {
+    $this->_lines($lines, '>');
+  }
+  function _deleted($lines) {
+    $this->_lines($lines, '<');
+  }
+
+  function _changed($orig, $closing) {
+    $this->_deleted($orig);
+    echo "---\n";
+    $this->_added($closing);
+  }
+}
+
+
+/**
+ *  Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;');      // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+  function _HWLDF_WordAccumulator() {
+    $this->_lines = array();
+    $this->_line = '';
+    $this->_group = '';
+    $this->_tag = '';
+  }
+
+  function _flushGroup($new_tag) {
+    if ($this->_group !== '') {
+      if ($this->_tag == 'mark') {
+        $this->_line .= '<span class="diffchange">' . check_plain($this->_group) . '</span>';
+      }
+      else {
+        $this->_line .= check_plain($this->_group);
+      }
+    }
+    $this->_group = '';
+    $this->_tag = $new_tag;
+  }
+
+  function _flushLine($new_tag) {
+    $this->_flushGroup($new_tag);
+    if ($this->_line != '') {
+      array_push($this->_lines, $this->_line);
+    }
+    else {
+      // make empty lines visible by inserting an NBSP
+      array_push($this->_lines, NBSP);
+    }
+    $this->_line = '';
+  }
+
+  function addWords($words, $tag = '') {
+    if ($tag != $this->_tag) {
+      $this->_flushGroup($tag);
+    }
+    foreach ($words as $word) {
+      // new-line should only come as first char of word.
+      if ($word == '') {
+        continue;
+      }
+      if ($word[0] == "\n") {
+        $this->_flushLine($tag);
+        $word = drupal_substr($word, 1);
+      }
+      assert(!strstr($word, "\n"));
+      $this->_group .= $word;
+    }
+  }
+
+  function getLines() {
+    $this->_flushLine('~done');
+    return $this->_lines;
+  }
+}
+
+/**
+ * @todo document
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+  function MAX_LINE_LENGTH() {
+    return 10000;
+  }
+
+  function WordLevelDiff($orig_lines, $closing_lines) {
+    list($orig_words, $orig_stripped) = $this->_split($orig_lines);
+    list($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+    $this->MappedDiff($orig_words, $closing_words, $orig_stripped, $closing_stripped);
+  }
+
+  function _split($lines) {
+    $words = array();
+    $stripped = array();
+    $first = TRUE;
+    foreach ($lines as $line) {
+      // If the line is too long, just pretend the entire line is one big word
+      // This prevents resource exhaustion problems
+      if ( $first ) {
+        $first = FALSE;
+      }
+      else {
+        $words[] = "\n";
+        $stripped[] = "\n";
+      }
+      if ( drupal_strlen( $line ) > $this->MAX_LINE_LENGTH() ) {
+        $words[] = $line;
+        $stripped[] = $line;
+      }
+      else {
+        if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', $line, $m)) {
+          $words = array_merge($words, $m[0]);
+          $stripped = array_merge($stripped, $m[1]);
+        }
+      }
+    }
+    return array($words, $stripped);
+  }
+
+  function orig() {
+    $orig = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $orig->addWords($edit->orig);
+      }
+      elseif ($edit->orig) {
+        $orig->addWords($edit->orig, 'mark');
+      }
+    }
+    $lines = $orig->getLines();
+    return $lines;
+  }
+
+  function closing() {
+    $closing = new _HWLDF_WordAccumulator;
+
+    foreach ($this->edits as $edit) {
+      if ($edit->type == 'copy') {
+        $closing->addWords($edit->closing);
+      }
+      elseif ($edit->closing) {
+        $closing->addWords($edit->closing, 'mark');
+      }
+    }
+    $lines = $closing->getLines();
+    return $lines;
+  }
+}
+
+/**
+ * Diff formatter which uses Drupal theme functions.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffFormatter extends DiffFormatter {
+
+  var $rows;
+  var $line_stats = array(
+    'counter' => array('x' => 0, 'y' => 0),
+    'offset' => array('x' => 0, 'y' => 0),
+  );
+
+  function DrupalDiffFormatter() {
+    $this->leading_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_leading');
+    $this->trailing_context_lines = \Drupal::config('diff.settings')->get('diff_context_lines_trailing');
+  }
+
+  function _start_diff() {
+    $this->rows = array();
+  }
+
+  function _end_diff() {
+    return $this->rows;
+  }
+
+  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+    return array(
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
+        'colspan' => 2,
+      ),
+      array(
+        'data' => _theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
+        'colspan' => 2,
+      )
+    );
+  }
+
+  function _start_block($header) {
+    if ($this->show_header) {
+      $this->rows[] = $header;
+    }
+  }
+
+  function _end_block() {
+  }
+
+  function _lines($lines, $prefix=' ', $color='white') {
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function addedLine($line) {
+    return array(
+      array(
+        'data' => '+',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-addedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function deletedLine($line) {
+    return array(
+      array(
+        'data' => '-',
+        'class' => 'diff-marker',
+      ),
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context diff-deletedline',
+      )
+    );
+  }
+
+  /**
+   * Note: you should HTML-escape parameter before calling this.
+   */
+  function contextLine($line) {
+    return array(
+      '&nbsp;',
+      array(
+        'data' => _theme('diff_content_line', array('line' => $line)),
+        'class' => 'diff-context',
+      )
+    );
+  }
+
+  function emptyLine() {
+    return array(
+      '&nbsp;',
+      _theme('diff_empty_line', array('line' => '&nbsp;')),
+    );
+  }
+
+  function _added($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine(check_plain($line)));
+    }
+  }
+
+  function _deleted($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->deletedLine(check_plain($line)), $this->emptyLine());
+    }
+  }
+
+  function _context($lines) {
+    foreach ($lines as $line) {
+      $this->rows[] = array_merge($this->contextLine(check_plain($line)), $this->contextLine(check_plain($line)));
+    }
+  }
+
+  function _changed($orig, $closing) {
+    $diff = new WordLevelDiff($orig, $closing);
+    $del = $diff->orig();
+    $add = $diff->closing();
+
+    // Notice that WordLevelDiff returns HTML-escaped output.
+    // Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+    while ($line = array_shift($del)) {
+      $aline = array_shift( $add );
+      $this->rows[] = array_merge($this->deletedLine($line), isset($aline) ? $this->addedLine($aline) : $this->emptyLine());
+    }
+    foreach ($add as $line) {  // If any leftovers
+      $this->rows[] = array_merge($this->emptyLine(), $this->addedLine($line));
+    }
+  }
+}
+
+/**
+ * Drupal inline Diff formatter.
+ * @private
+ * @subpackage DifferenceEngine
+ */
+class DrupalDiffInline {
+  var $a;
+  var $b;
+
+  /**
+   * Constructor.
+   */
+  function __construct($a, $b) {
+    $this->a = $a;
+    $this->b = $b;
+  }
+
+  /**
+   * Render differences inline using HTML markup.
+   */
+  function render() {
+    $a = preg_split('/(<[^>]+?>| )/', $this->a, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $b = preg_split('/(<[^>]+?>| )/', $this->b, -1, PREG_SPLIT_DELIM_CAPTURE);
+    $diff = new Diff($a, $b);
+    $diff->edits = $this->process_edits($diff->edits);
+
+    // Assemble highlighted output
+    $output = '';
+    foreach ($diff->edits as $chunk) {
+      switch ($chunk->type) {
+        case 'copy':
+          $output .= implode('', $chunk->closing);
+          break;
+        case 'delete':
+          foreach ($chunk->orig as $i => $piece) {
+            if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+        default:
+          $chunk->closing = $this->process_chunk($chunk->closing);
+          foreach ($chunk->closing as $i => $piece) {
+            if ($piece === ' ' || (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>' && drupal_strtolower(drupal_substr($piece, 1, 3)) != 'img')) {
+              $output .= $piece;
+            }
+            else {
+              $output .= _theme('diff_inline_chunk', array('text' => $piece, 'type' => $chunk->type));
+            }
+          }
+          break;
+      }
+    }
+    return $output;
+  }
+
+  /**
+   * Merge chunk segments between tag delimiters.
+   */
+  function process_chunk($chunk) {
+    $processed = array();
+    $j = 0;
+    foreach ($chunk as $i => $piece) {
+      $next = isset($chunk[$i+1]) ? $chunk[$i+1] : NULL;
+      if (!isset($processed[$j])) {
+        $processed[$j] = '';
+      }
+      if (strpos($piece, '<') === 0 && drupal_substr($piece, drupal_strlen($piece) - 1) === '>') {
+        $processed[$j] = $piece;
+        $j++;
+      }
+      elseif (isset($next) && strpos($next, '<') === 0 && drupal_substr($next, drupal_strlen($next) - 1) === '>') {
+        $processed[$j] .= $piece;
+        $j++;
+      }
+      else {
+        $processed[$j] .= $piece;
+      }
+    }
+    return $processed;
+  }
+
+  /**
+   * Merge copy and equivalent edits into intelligible chunks.
+   */
+  function process_edits($edits) {
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Make two passes -- first merge space delimiter copies back into their originals.
+    while ($chunk = array_shift($edits)) {
+      if ($chunk->type == 'copy' && $chunk->orig === array(' ')) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    // Initial setup
+    $edits = $processed;
+    $processed = array();
+    $current = array_shift($edits);
+
+    // Second, merge equivalent chunks into each other.
+    while ($chunk = array_shift($edits)) {
+      if ($current->type == $chunk->type) {
+        $current->orig = array_merge((array) $current->orig, (array) $chunk->orig);
+        $current->closing = array_merge((array) $current->closing, (array) $chunk->closing);
+      }
+      else {
+        $processed[] = $current;
+        $current = $chunk;
+      }
+    }
+    $processed[] = $current;
+
+    return $processed;
+  }
+}
