diff --git a/features.admin.inc b/features.admin.inc
index 4e12576..acb4e07 100644
--- a/features.admin.inc
+++ b/features.admin.inc
@@ -66,6 +66,69 @@ function features_settings_form($form, $form_state) {
'#description' => t('If you have a large site with many features, you may experience lag on full cache clear. If disabled, features will rebuild only when viewing the features list or saving the modules list.'),
);
+ $providers = features_diff_formatter_providers();
+ $form['diff'] = array(
+ '#title' => t('Diff formatter'),
+ '#type' => 'fieldset',
+ );
+
+ $form['diff']['diff'] = array(
+ '#type' => 'fieldset',
+ '#title' => 'Diff',
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ if (isset($providers['diff'])) {
+ $name = 'features_diff_formatter_provider__diff__context_lines';
+ $form['diff']['diff'][$name] = array(
+ '#type' => 'textfield',
+ '#title' => ('Lines'),
+ '#size' => 3,
+ '#default_value' => variable_get($name, 3),
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#attributes' => array(
+ 'class' => array('align-right'),
+ ),
+ );
+ }
+ else {
+ $form['diff']['diff']['install_guide'] = array(
+ '#markup' => '
' . t('Install the Diff module') . '
',
+ );
+ }
+
+ $form['diff']['jsdifflib'] = array(
+ '#type' => 'fieldset',
+ '#title' => 'jsdifflib',
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ );
+ if (isset($providers['jsdifflib'])) {
+ $name = 'features_diff_formatter_provider__jsdifflib__context_lines';
+ $form['diff']['jsdifflib'][$name] = array(
+ '#type' => 'textfield',
+ '#title' => ('Lines'),
+ '#size' => 3,
+ '#default_value' => variable_get($name, 3),
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#attributes' => array(
+ 'class' => array('align-right'),
+ ),
+ );
+
+ $name = 'features_diff_formatter_provider__jsdifflib__view_type';
+ $form['diff']['jsdifflib'][$name] = array(
+ '#type' => 'checkbox',
+ '#title' => ('Inline'),
+ '#default_value' => variable_get($name, 0),
+ );
+ }
+ else {
+ $form['diff']['jsdifflib']['install_guide'] = array(
+ '#markup' => '' . t('Extract the jsdifflib library to the libraries directory and rename it from jsdifflib-master to jsdifflib.') . '
',
+ );
+ }
+
return system_settings_form($form);
}
@@ -1332,39 +1395,263 @@ function features_cleanup_form($form, $form_state, $cache_clear = FALSE) {
* Themed display of what is different.
*/
function features_feature_diff($feature, $component = NULL) {
- drupal_add_css(drupal_get_path('module', 'features') . '/features.css');
module_load_include('inc', 'features', 'features.export');
+
drupal_set_title($feature->info['name']);
- $overrides = features_detect_overrides($feature);
+ $return = array();
- $output = '';
+ $overrides = features_detect_overrides($feature);
if (!empty($overrides)) {
+ $providers = features_diff_formatter_providers();
+ $provider = features_diff_formatter_default_provider();
+
+ // Add the diff formatter settings form if we have more than one formatter
+ // or have any settings of the single one.
+ if (!(count($providers) == 1 && !isset($provider['settings']))) {
+ $return['features_diff_formatter_settings_form'] = drupal_get_form('features_diff_formatter_settings_form');
+ $return['features_diff_formatter_settings_form']['#weight'] = 0;
+ }
+
// Filter overrides down to specified component.
if (isset($component) && isset($overrides[$component])) {
$overrides = array($component => $overrides[$component]);
}
- module_load_include('inc', 'diff', 'diff.engine');
- $formatter = new DrupalDiffFormatter();
+ if ($provider['name'] == 'diff') {
+ $return['feature_diff'] = features_feature_diff_diff($overrides, $provider);
+ }
+ elseif ($provider['name'] == 'jsdifflib') {
+ $return['feature_diff'] = features_feature_diff_jsdifflib($overrides, $provider);
+ }
- $rows = array();
- foreach ($overrides as $component => $items) {
- $rows[] = array(array('data' => $component, 'colspan' => 4, 'header' => TRUE));
- $diff = new Diff(explode("\n", $items['default']), explode("\n", $items['normal']));
- $rows = array_merge($rows, $formatter->format($diff));
+ if (isset($return['feature_diff'])) {
+ $return['feature_diff']['#weight'] = 1;
+ $return['feature_diff']['#formatter_provider'] = $provider;
}
- $header = array(
+ }
+
+ return $return;
+}
+
+function features_feature_diff_diff($overrides, $provider) {
+ module_load_include('inc', 'diff', 'diff.engine');
+ $formatter = new DrupalDiffFormatter();
+
+ $return = array(
+ '#theme' => 'table',
+ '#header' => array(
array('data' => t('Default'), 'colspan' => 2),
array('data' => t('Overrides'), 'colspan' => 2),
+ ),
+ '#rows' => array(),
+ '#empty' => t('No changes have been made to this feature.'),
+ '#weight' => 1,
+ '#attributes' => array(
+ 'class' => array('diff', 'features-diff'),
+ ),
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#attached' => array(
+ 'css' => array(
+ drupal_get_path('module', 'features') . '/features.css'
+ ),
+ ),
+ );
+
+ $formatter->leading_context_lines
+ = $formatter->trailing_context_lines
+ = $provider['settings']['context_lines'];
+
+ $return['#attributes']['class'][] = 'diff-indent';
+
+ foreach ($overrides as $component => $items) {
+ $return['#rows'][] = array(
+ array(
+ 'data' => $component,
+ 'colspan' => 4,
+ 'header' => TRUE,
+ ),
+ );
+ $diff = new Diff(explode("\n", $items['default']), explode("\n", $items['normal']));
+
+ $return['#rows'] = array_merge(
+ $return['#rows'],
+ $formatter->format($diff)
);
- $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('diff', 'features-diff'))));
}
- else {
- $output = "" . t('No changes have been made to this feature.') . "
";
+
+ return $return;
+}
+
+function features_feature_diff_jsdifflib($overrides, $provider) {
+ $jsdifflib_path = libraries_get_path('jsdifflib');
+ $path = drupal_get_path('module', 'features');
+ $return = array(
+ '#attached' => array(
+ 'css' => array(
+ "$jsdifflib_path/diffview.css",
+ ),
+ 'js' => array(
+ "$jsdifflib_path/difflib.js",
+ "$jsdifflib_path/diffview.js",
+ "$path/js/features.diff-formatter-jsdifflib.js",
+ ),
+ ),
+ '#weight' => 1,
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ $return['components'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ $return['formatted'] = array(
+ '#markup' => '',
+ );
+
+ foreach ($overrides as $component => $item) {
+ $attributes = array(
+ 'title' => $component,
+ 'class' => array(
+ 'component',
+ 'component-' . drupal_html_class($component),
+ ),
+ );
+ $return['components'][$component] = array(
+ '#prefix' => '',
+ '#suffix' => '
'
+ );
+
+ $return['components'][$component]['default'] = array(
+ '#markup' => '' . check_plain($item['default']) . '
',
+ );
+
+ $return['components'][$component]['normal'] = array(
+ '#markup' => '' . check_plain($item['normal']) . '
',
+ );
+ }
+
+ return $return;
+}
+
+/**
+ * Form builder function for 'lines of context' above diff output.
+ */
+function features_diff_formatter_settings_form($form, &$form_state) {
+ $form['#attributes']['class'][] = 'features';
+ $form['#attached']['css'][] = drupal_get_path('module', 'features') . '/features.css';
+
+ $providers = features_diff_formatter_providers();
+
+ $default_provider = features_diff_formatter_default_provider();
+
+ $form['provider'] = array(
+ '#type' => 'radios',
+ '#required' => TRUE,
+ '#title' => t('Provider'),
+ '#title_display' => 'none',
+ '#default_value' => $default_provider['name'],
+ '#options' => features_diff_formatter_providers_options(),
+ '#attributes' => array(
+ 'class' => array('provider-selector'),
+ ),
+ );
+
+ if (count($form['provider']['#options']) == 1) {
+ $form['provider']['#attributes']['class'][] = 'element-invisible';
+ }
+
+ $form['providers'] = array(
+ '#tree' => TRUE,
+ );
+
+ if (isset($form['provider']['#options']['diff'])) {
+ $form['providers']['diff'] = array(
+ '#type' => 'container',
+ '#tree' => TRUE,
+ '#states' => array(
+ 'invisible' => array(
+ ':input[name="provider"]' => array('!value' => 'diff'),
+ ),
+ ),
+ '#attributes' => array(
+ 'class' => array('provider', 'provider-diff'),
+ ),
+ );
+
+ $form['providers']['diff']['context_lines'] = array(
+ '#type' => 'textfield',
+ '#title' => ('Lines'),
+ '#size' => 3,
+ '#default_value' => $providers['diff']['settings']['context_lines'],
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#attributes' => array(
+ 'class' => array('align-right'),
+ ),
+ );
+ }
+
+ if (isset($form['provider']['#options']['jsdifflib'])) {
+ $form['providers']['jsdifflib'] = array(
+ '#type' => 'container',
+ '#tree' => TRUE,
+ '#states' => array(
+ 'invisible' => array(
+ ':input[name="provider"]' => array('!value' => 'jsdifflib'),
+ ),
+ ),
+ '#attributes' => array(
+ 'class' => array('provider', 'provider-jsdifflib'),
+ ),
+ );
+
+ $form['providers']['jsdifflib']['context_lines'] = array(
+ '#type' => 'textfield',
+ '#title' => ('Lines'),
+ '#size' => 3,
+ '#default_value' => $providers['jsdifflib']['settings']['context_lines'],
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#attributes' => array(
+ 'class' => array('align-right'),
+ ),
+ );
+
+ $form['providers']['jsdifflib']['view_type'] = array(
+ '#type' => 'checkbox',
+ '#title' => ('Inline'),
+ '#default_value' => $providers['jsdifflib']['settings']['view_type'],
+ );
+ }
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ 'submit' => array(
+ '#type' => 'submit',
+ '#value' => t('Reload page'),
+ ),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit function for features_diff_formatter_settings_form.
+ */
+function features_diff_formatter_settings_form_submit($form, &$form_state) {
+ $provider_name = $form_state['values']['provider'];
+ $_SESSION['features_diff_formatter_provider'] = $provider_name;
+
+ $provider = features_diff_formatter_provider_load($provider_name);
+ if (isset($provider['settings'])) {
+ foreach (array_keys($provider['settings']) as $key) {
+ if (isset($form_state['values']['providers'][$provider_name][$key])) {
+ $_SESSION["features_diff_formatter_provider__{$provider_name}__{$key}"] = $form_state['values']['providers'][$provider_name][$key];
+ }
+ }
}
- $output = array('page' => array('#markup' => "{$output}
"));
- return $output;
}
/**
diff --git a/features.css b/features.css
index 16c7f72..cc32b61 100644
--- a/features.css
+++ b/features.css
@@ -69,6 +69,32 @@ div.features-empty {
color:#999;
}
+.features-diff-formatter-settings-form .provider-selector,
+.features-diff-formatter-settings-form .provider,
+.features-diff-formatter-settings-form .form-type-textfield,
+.features-diff-formatter-settings-form .form-type-textfield label,
+.features-diff-formatter-settings-form .form-type-textfield .form-text,
+.features-diff-formatter-settings-form .form-type-checkbox {
+ float: left;
+}
+
+.features-diff-formatter-settings-form .provider-selector,
+.features-diff-formatter-settings-form .provider {
+ margin-right: 2em;
+}
+
+.features-diff-formatter-settings-form .form-type-checkbox {
+ margin: 0 0 0 2em;
+}
+
+.features-diff {
+ clear: both;
+}
+
+.features .align-right {
+ text-align: right;
+}
+
form div.buttons {
text-align:center;
}
@@ -289,6 +315,10 @@ span.features-component-list .features-dependency {
background:#f8f8f8;
}
+table.diff-indent td div {
+ white-space: pre;
+}
+
/**
* Features diff.
*/
@@ -563,4 +593,4 @@ input.form-submit.features-refresh-button {
fieldset.features-export-component .fieldset-title .component-count {
font-size: 12px;
font-weight: bold;
-}
\ No newline at end of file
+}
diff --git a/features.install b/features.install
index 762a054..117b238 100644
--- a/features.install
+++ b/features.install
@@ -25,6 +25,14 @@ function features_uninstall() {
variable_del('features_default_export_path');
variable_del('features_semaphore');
variable_del('features_ignored_orphans');
+ foreach (features_diff_formatter_providers() as $provider) {
+ if (isset($provider['settings'])) {
+ foreach (array_keys($provider['settings']) as $key) {
+ variable_del("features_diff_formatter_provider__{$provider['name']}__{$key}");
+ }
+ }
+ }
+
if (db_table_exists('menu_custom')) {
db_delete('menu_custom')
->condition('menu_name', 'features')
diff --git a/features.module b/features.module
index ba5b9fe..fcabf5f 100644
--- a/features.module
+++ b/features.module
@@ -154,7 +154,7 @@ function features_menu() {
'file' => "features.admin.inc",
'weight' => 11,
);
- if (module_exists('diff')) {
+ if (features_diff_formatter_providers()) {
$items['admin/structure/features/%feature/diff'] = array(
'title' => 'Review overrides',
'description' => 'Compare default and current feature.',
@@ -1109,3 +1109,80 @@ function features_get_deprecated($components = array()) {
}
return $deprecated;
}
+
+function features_diff_formatter_provider_load($provider_name) {
+ $providers = features_diff_formatter_providers();
+
+ return isset($providers[$provider_name]) ? $providers[$provider_name] : NULL;
+}
+
+/**
+ * Collect the available diff providers.
+ *
+ * @return array
+ * Array of diff provider properties.
+ * - name: Machine name.
+ * - title: Human name.
+ */
+function features_diff_formatter_providers() {
+ $providers = array();
+
+ if (module_exists('diff')) {
+ $providers['diff'] = array(
+ 'name' => 'diff',
+ 'title' => t('Default'),
+ 'settings' => array(
+ 'context_lines' => 3,
+ ),
+ );
+ }
+
+ if (module_exists('libraries') && libraries_get_path('jsdifflib')) {
+ $providers['jsdifflib'] = array(
+ 'name' => 'jsdifflib',
+ 'title' => t('JS Diff lib'),
+ 'settings' => array(
+ 'context_lines' => 3,
+ 'view_type' => 1,
+ ),
+ );
+ }
+
+ foreach ($providers as &$provider) {
+ if (isset($provider['settings'])) {
+ foreach ($provider['settings'] as $key => $value) {
+ $name = "features_diff_formatter_provider__{$provider['name']}__{$key}";
+ $provider['settings'][$key] = (array_key_exists($name, $_SESSION)) ? $_SESSION[$name] : variable_get($name, $value);
+ }
+ }
+ }
+
+ return $providers;
+}
+
+function features_diff_formatter_providers_options() {
+ $options = array();
+
+ foreach (features_diff_formatter_providers() as $provider) {
+ $options[$provider['name']] = $provider['title'];
+ }
+
+ return $options;
+}
+
+function features_diff_formatter_default_provider() {
+ $providers = features_diff_formatter_providers();
+ $name = 'features_diff_formatter_provider';
+ if (isset($_SESSION[$name]) && isset($providers[$_SESSION[$name]])) {
+ return $providers[$_SESSION[$name]];
+ }
+ else {
+ $default_provider = variable_get($name, NULL);
+ if (isset($providers[$default_provider])) {
+ return $providers[$default_provider];
+ }
+ else {
+ return reset($providers);
+ }
+ }
+}
diff --git a/js/features.diff-formatter-jsdifflib.js b/js/features.diff-formatter-jsdifflib.js
new file mode 100644
index 0000000..fc33fc4
--- /dev/null
+++ b/js/features.diff-formatter-jsdifflib.js
@@ -0,0 +1,104 @@
+/**
+ * @file
+ * Diff formatter by jsdifflib.
+ */
+
+(function ($) {
+ 'use strict';
+ Drupal.behaviors.featuresDiffFormatterJsdifflib = {
+ attach: function (context, settings) {
+ if (difflib === undefined || diffview === undefined) {
+ return;
+ }
+
+ if (Drupal.featuresDiffFormatterJsdifflib.settingsContainer === null) {
+ var $settingsContainer = $('form.features-diff-formatter-settings-form .provider-jsdifflib:not(.features-diff-formatter-jsdifflib-processed)');
+ if ($settingsContainer.length === 1) {
+ Drupal.featuresDiffFormatterJsdifflib.settingsContainer = $settingsContainer;
+ $settingsContainer.addClass('features-diff-formatter-jsdifflib-processed');
+ $(':input', $settingsContainer).change(Drupal.featuresDiffFormatterJsdifflib.settingsOnChange);
+ }
+ }
+
+ if (Drupal.featuresDiffFormatterJsdifflib.settings === null) {
+ Drupal.featuresDiffFormatterJsdifflib.settingsUpdate();
+ }
+
+ var $formatted = $('.features-diff-formatter-provider-jsdifflib:not(.features-diff-formatter-jsdifflib-processed)', context);
+ if ($formatted.length) {
+ $formatted.addClass('features-diff-formatter-jsdifflib-processed');
+ Drupal.featuresDiffFormatterJsdifflib.formattedUpdate($formatted);
+ }
+ }
+ };
+
+ Drupal.featuresDiffFormatterJsdifflib = Drupal.featuresDiffFormatterJsdifflib || {};
+
+ Drupal.featuresDiffFormatterJsdifflib.settings = null;
+
+ Drupal.featuresDiffFormatterJsdifflib.settingsContainer = null;
+
+ Drupal.featuresDiffFormatterJsdifflib.settingsUpdate = function () {
+ var
+ settings = {
+ baseTextName: Drupal.t('Default', {}, {}),
+ newTextName: Drupal.t('Override', {}, {}),
+ contextSize: 3,
+ viewType: 0
+ },
+ $container = Drupal.featuresDiffFormatterJsdifflib.settingsContainer;
+
+ if ($container !== null) {
+ var
+ $contextSize = $(':input[name=$"[context_lines]"]', $container),
+ $viewType = $(':input[name=$"[view_type]"]', $container);
+
+ settings.contextSize = parseInt($contextSize.val(), 10);
+ if (isNaN(settings.contextSize) || settings.contextSize < 1) {
+ settings.contextSize = 3;
+ $contextSize.val(settings.contextSize);
+ }
+
+ settings.viewType = $viewType.is(':checked') ? 1 : 0;
+ }
+
+ Drupal.featuresDiffFormatterJsdifflib.settings = settings;
+ };
+
+ Drupal.featuresDiffFormatterJsdifflib.settingsOnChange = function () {
+ Drupal.featuresDiffFormatterJsdifflib.settingsUpdate();
+ Drupal.featuresDiffFormatterJsdifflib.formattedUpdate($('.features-diff-formatter-provider-jsdifflib'));
+ };
+
+ Drupal.featuresDiffFormatterJsdifflib.formattedUpdate = function ($containers) {
+ var options = Drupal.featuresDiffFormatterJsdifflib.settings;
+
+ $containers.each(function () {
+ var
+ $container = $(this),
+ $formatted = $('> .formatted', $container);
+
+ $formatted.empty();
+
+ $('> .components > .component', $container).each(function () {
+ var
+ componentName = $(this).attr('title'),
+ textDefaultDecoded = $('').html($('> .diff-default', $(this)).html()).text(),
+ textOverrideDecoded = $('').html($('> .diff-normal', $(this)).html()).text();
+
+ options.baseTextLines = difflib.stringAsLines(textDefaultDecoded);
+ options.newTextLines = difflib.stringAsLines(textOverrideDecoded);
+
+ var sm = new difflib.SequenceMatcher(
+ options.baseTextLines,
+ options.newTextLines
+ );
+ options.opcodes = sm.get_opcodes();
+
+ $formatted.append($('' + componentName + '
'));
+ $formatted.append(diffview.buildView(options));
+ });
+ });
+ };
+
+}(jQuery));