diff --git a/features.admin.inc b/features.admin.inc index 4e12576..4d3bbf8 100644 --- a/features.admin.inc +++ b/features.admin.inc @@ -66,6 +66,74 @@ 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' => 'select', + '#title' => ('View type'), + '#required' => TRUE, + '#default_value' => variable_get($name, 'side_by_side'), + '#options' => array( + 'inline' => t('Inline'), + 'side_by_side' => t('Side by side'), + ), + ); + } + 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 +1400,333 @@ 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); + } + + if (isset($return['feature_diff'])) { + $return['feature_diff']['#weight'] = 2; + $return['feature_diff']['#formatter_provider'] = $provider; + } + } + + return $return; +} + +function features_feature_diff_diff($overrides, $provider) { + module_load_include('inc', 'diff', 'diff.engine'); + + $formatter = new DrupalDiffFormatter(); + $formatter->leading_context_lines + = $formatter->trailing_context_lines + = $provider['settings']['context_lines']; + + $path = drupal_get_path('module', 'features'); + $return = array( + '#prefix' => '
', + '#suffix' => '
', + '#attached' => array( + 'css' => array( + "$path/features.css", + ), + 'js' => array( + "$path/js/features.diff-formatter-diff.js", + ), + 'library' => array( + array('system', 'ui.tabs'), + ), + ), + ); + + $return['formatted'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + + if (count($overrides) > 1) { + $return['formatted']['navigator'] = array( + '#type' =>'markup', + '#prefix' =>'', + ); - $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)); + foreach (array_keys($overrides) as $component) { + $return['formatted']['navigator']['#markup'] .= format_string( + '
  • @component
  • ', + array('@component' => $component) + ); } - $header = array( + } + + $table = array( + 'caption' => NULL, + 'sticky' => FALSE, + 'header' => array( array('data' => t('Default'), 'colspan' => 2), array('data' => t('Overrides'), 'colspan' => 2), + ), + 'rows' => array(), + 'attributes' => array('class' => array('diff', 'diff-indent', 'features-diff')), + ); + + foreach ($overrides as $component => $items) { + $return['#rows'][] = array( + array( + 'data' => $component, + 'colspan' => 4, + 'header' => TRUE, + ), + ); + + $return['formatted'][$component] = array( + '#prefix' => format_string('
    ', array('@component' => $component)), + '#suffix' => '
    ', + ); + + $return['formatted'][$component]['title'] = array( + '#markup' => '

    ' . $component . '

    ', + ); + + $diff = new Diff(explode("\n", $items['default']), explode("\n", $items['normal'])); + $table['rows'] = $formatter->format($diff); + $return['formatted'][$component]['diff'] = array( + '#markup' => theme('table', $table), ); - $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", + ), + 'library' => array( + array('system', 'ui.tabs'), + ), + ), + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + + $return['components'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + + $return['formatted'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + + if (count($overrides) > 1) { + $return['formatted']['navigator'] = array( + '#type' =>'markup', + '#prefix' =>'', + ); + + foreach (array_keys($overrides) as $component) { + $return['formatted']['navigator']['#markup'] .= format_string( + '
  • @component
  • ', + array('@component' => $component) + ); + } + } + + 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['formatted'][$component] = array( + '#type' => 'markup', + '#markup' => format_string( + '

    @component

    ', + array('@component' => $component) + ), + ); + } + + 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['#attributes']['class'][] = 'features-diff-formatter-settings-form'; + $form['#attributes']['class'][] = 'clearfix'; + $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' => 'select', + '#title' => ('View type'), + '#title_display' => 'none', + '#required' => TRUE, + '#default_value' => $providers['jsdifflib']['settings']['view_type'], + '#options' => array( + 'inline' => t('Inline'), + 'side_by_side' => t('Side by side'), + ), + ); + } + + $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..5556421 100644 --- a/features.css +++ b/features.css @@ -69,6 +69,24 @@ 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-select { + float: left; +} + +.features-diff-formatter-settings-form .provider-selector, +.features-diff-formatter-settings-form .provider .form-item { + margin-right: 2em; +} + +.features .align-right { + text-align: right; +} + form div.buttons { text-align:center; } @@ -277,18 +295,59 @@ span.admin-conflict { color:#c30; } -table.features-diff td.diff-addedline, +table.features-diff tbody td:nth-child(2):nth-last-child(1), +table.features-diff tbody td:nth-child(1), +table.features-diff tbody td:nth-child(3) { + background: #eed; + border: 1px solid #bbc; + padding: .3em .5em .1em 2em; + vertical-align: top; +} + +table.diff tbody tr:nth-child(odd), +table.features-diff tr.odd { + background-color: #ffffff; +} + +table.diff tbody tr:nth-child(even), +table.features-diff tr.even { + background-color: #fafafa; +} + +table.features-diff tr.odd td.diff-deletedline { + background-color: #ee9999; +} + +table.features-diff tr.even td.diff-deletedline { + background-color: #e89999; +} + +table.features-diff tr.odd td.diff-addedline { + background-color: #99ee99; +} + +table.features-diff tr.even td.diff-addedline { + background-color: #99e899; +} + +table.features-diff .diffchange { + font-weight: bold; +} + span.features-component-list .features-detected { color:#68a; background:#def; } -table.features-diff td.diff-deletedline, span.features-component-list .features-dependency { color:#999; background:#f8f8f8; } +table.diff-indent td div { + white-space: pre; +} + /** * Features diff. */ @@ -304,7 +363,7 @@ table.features-diff td.diff-deletedline, table.features-diff td.diff-addedline, table.features-diff td.diff-context { width:50%; - font-family:'Andale Mono',monospace; + font-family: 'Andale Mono', 'DejaVu Sans Mono', 'Courier New', monospace; } /** @@ -563,4 +622,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..69fb4ed 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] = (isset($_SESSION[$name])) ? $_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-diff.js b/js/features.diff-formatter-diff.js new file mode 100644 index 0000000..facdfd4 --- /dev/null +++ b/js/features.diff-formatter-diff.js @@ -0,0 +1,27 @@ +/** + * @file + * Documentation missing. + */ + +(function ($) { + 'use strict'; + Drupal.behaviors.featuresDiffFormatterDiff = { + attach: function (context, settings) { + var $wrapper = $('.features-diff-formatter-provider-diff:not(.features-diff-formatter-diff-processed)', context); + if ($wrapper.length) { + $wrapper.addClass('features-diff-formatter-diff-processed'); + if ($('> .formatted > .diff-navigator', $wrapper).length) { + $('> .formatted', $wrapper).tabs(); + Drupal.featuresDiffFormatterDiff.hideTitles($wrapper); + } + } + } + }; + + Drupal.featuresDiffFormatterDiff = Drupal.featuresDiffFormatterDiff || {}; + + Drupal.featuresDiffFormatterDiff.hideTitles = function ($wrapper) { + $('h3', $wrapper).addClass('js-hide'); + }; + +}(jQuery)); diff --git a/js/features.diff-formatter-jsdifflib.js b/js/features.diff-formatter-jsdifflib.js new file mode 100644 index 0000000..6623636 --- /dev/null +++ b/js/features.diff-formatter-jsdifflib.js @@ -0,0 +1,114 @@ +/** + * @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 $wrapper = $('.features-diff-formatter-provider-jsdifflib:not(.features-diff-formatter-jsdifflib-processed)', context); + if ($wrapper.length) { + $wrapper.addClass('features-diff-formatter-jsdifflib-processed'); + Drupal.featuresDiffFormatterJsdifflib.formattedUpdate($wrapper); + + if ($('> .formatted > .diff-navigator', $wrapper).length) { + $('> .formatted', $wrapper).tabs(); + Drupal.featuresDiffFormatterJsdifflib.hideTitles($wrapper); + } + } + } + }; + + 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.val() === 'inline' ? 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), + $rawComponents = $('> .components > .component', $container), + $formatted = $('> .formatted', $container); + + $formatted.find('> .diff-component > table.diff').remove(); + + $rawComponents.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(); + + $('> #diff-component-' + componentName, $formatted) + .append(diffview.buildView(options)); + }); + }); + }; + + Drupal.featuresDiffFormatterJsdifflib.hideTitles = function ($wrapper) { + $('h3', $wrapper).addClass('js-hide'); + }; + +}(jQuery));