diff.pages.inc 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. <?php
  2. /**
  3. * @file
  4. * Menu callbacks for hook_menu().
  5. */
  6. /**
  7. * Menu callback - show latest diff for a given node.
  8. */
  9. function diff_latest($node) {
  10. $revisions = node_revision_list($node);
  11. $new = array_shift($revisions);
  12. $old = array_shift($revisions);
  13. drupal_goto("node/{$node->nid}/revisions/view/{$old->vid}/{$new->vid}");
  14. }
  15. /**
  16. * Menu callback - an overview table of older revisions.
  17. *
  18. * Generate an overview table of older revisions of a node and provide
  19. * an input form to select two revisions for a comparison.
  20. */
  21. function diff_diffs_overview($node) {
  22. drupal_set_title(t('Revisions for %title', array('%title' => $node->title)), PASS_THROUGH);
  23. return drupal_get_form('diff_node_revisions', $node);
  24. }
  25. /**
  26. * Input form to select two revisions.
  27. */
  28. function diff_node_revisions($form, $form_state, $node) {
  29. $form['nid'] = array(
  30. '#type' => 'hidden',
  31. '#value' => $node->nid,
  32. );
  33. $revision_list = node_revision_list($node);
  34. if (count($revision_list) > REVISION_LIST_SIZE) {
  35. // If the list of revisions is longer than the number shown on one page
  36. // split the array.
  37. $page = isset($_GET['page']) ? $_GET['page'] : '0';
  38. $revision_chunks = array_chunk(node_revision_list($node), REVISION_LIST_SIZE);
  39. $revisions = $revision_chunks[$page];
  40. // Set up global pager variables as would 'pager_query' do.
  41. // These variables are then used in the theme('pager') call later.
  42. global $pager_page_array, $pager_total, $pager_total_items;
  43. $pager_total_items[0] = count($revision_list);
  44. $pager_total[0] = ceil(count($revision_list) / REVISION_LIST_SIZE);
  45. $pager_page_array[0] = max(0, min($page, ((int) $pager_total[0]) - 1));
  46. }
  47. else {
  48. $revisions = $revision_list;
  49. }
  50. $revert_permission = FALSE;
  51. if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
  52. $revert_permission = TRUE;
  53. }
  54. $delete_permission = FALSE;
  55. if ((user_access('delete revisions') || user_access('administer nodes')) && node_access('delete', $node)) {
  56. $delete_permission = TRUE;
  57. }
  58. foreach ($revisions as $revision) {
  59. $operations = array();
  60. $revision_ids[$revision->vid] = '';
  61. $revision_log = ($revision->log != '') ? '<p class="revision-log">' . filter_xss($revision->log) . '</p>' : '';
  62. if ($revision->current_vid > 0) {
  63. $form['info'][$revision->vid] = array(
  64. '#markup' => t('!date by !username', array(
  65. '!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"),
  66. '!username' => theme('username', array('account' => $revision)))) . $revision_log,
  67. );
  68. }
  69. else {
  70. $diff_date = l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view");
  71. $form['info'][$revision->vid] = array(
  72. '#markup' => t('!date by !username', array(
  73. '!date' => $diff_date,
  74. '!username' => theme('username', array('account' => $revision)))
  75. ) . $revision_log,
  76. );
  77. if ($revert_permission) {
  78. $operations[] = array(
  79. '#markup' => l(t('Revert'), "node/$node->nid/revisions/$revision->vid/revert"),
  80. );
  81. }
  82. if ($delete_permission) {
  83. $operations[] = array(
  84. '#markup' => l(t('Delete'), "node/$node->nid/revisions/$revision->vid/delete"),
  85. );
  86. }
  87. // Set a dummy, even if the user has no permission for the other
  88. // operations, so that we can check if the operations array is
  89. // empty to know if the row denotes the current revision.
  90. $operations[] = array();
  91. }
  92. $form['operations'][$revision->vid] = $operations;
  93. }
  94. $new_vid = key($revision_ids);
  95. next($revision_ids);
  96. $old_vid = key($revision_ids);
  97. $form['diff']['old'] = array(
  98. '#type' => 'radios',
  99. '#options' => $revision_ids,
  100. '#default_value' => $old_vid,
  101. );
  102. $form['diff']['new'] = array(
  103. '#type' => 'radios',
  104. '#options' => $revision_ids,
  105. '#default_value' => $new_vid,
  106. );
  107. $form['submit'] = array('#type' => 'submit', '#value' => t('Compare'));
  108. if (count($revision_list) > REVISION_LIST_SIZE) {
  109. $form['#suffix'] = theme('pager');
  110. }
  111. $form['#attached'] = diff_build_attachments(TRUE);
  112. return $form;
  113. }
  114. /**
  115. * Submit code for input form to select two revisions.
  116. */
  117. function diff_node_revisions_submit($form, &$form_state) {
  118. // The ids are ordered so the old revision is always on the left.
  119. $old_vid = min($form_state['values']['old'], $form_state['values']['new']);
  120. $new_vid = max($form_state['values']['old'], $form_state['values']['new']);
  121. $form_state['redirect'] = 'node/' . $form_state['values']['nid'] . '/revisions/view/' . $old_vid . '/' . $new_vid;
  122. }
  123. /**
  124. * Validation for input form to select two revisions.
  125. */
  126. function diff_node_revisions_validate($form, &$form_state) {
  127. $old_vid = $form_state['values']['old'];
  128. $new_vid = $form_state['values']['new'];
  129. if ($old_vid == $new_vid || !$old_vid || !$new_vid) {
  130. form_set_error('diff', t('Select different revisions to compare.'));
  131. }
  132. }
  133. /**
  134. * Create a comparison for the node between versions 'old_vid' and 'new_vid'.
  135. *
  136. * @param object $node
  137. * Node on which to perform comparison
  138. * @param integer $old_vid
  139. * Version ID of the old revision.
  140. * @param integer $new_vid
  141. * Version ID of the new revision.
  142. */
  143. function diff_diffs_show($node, $old_vid, $new_vid, $state = NULL) {
  144. // Attaches the CSS.
  145. $build['#attached'] = diff_build_attachments();
  146. $default_state = variable_get('diff_default_state_node', 'raw');
  147. if (empty($state)) {
  148. $state = $default_state;
  149. }
  150. $state = str_replace('-', '_', $state);
  151. if (!array_key_exists($state, diff_available_states())) {
  152. $state = $default_state;
  153. }
  154. // Same title as the 'Revisions' tab. Blocked by non-page requests.
  155. if (node_is_page($node)) {
  156. drupal_set_title(t('Revisions for %title', array('%title' => $node->title)), PASS_THROUGH);
  157. }
  158. $node_revisions = node_revision_list($node);
  159. $old_node = node_load($node->nid, $old_vid);
  160. $new_node = node_load($node->nid, $new_vid);
  161. // Generate table header (date, username, log message).
  162. $old_header = t('!date by !username', array(
  163. '!date' => l(format_date($old_node->revision_timestamp), "node/$node->nid/revisions/$old_node->vid/view", array('absolute' => 1)),
  164. '!username' => theme('username', array('account' => $node_revisions[$old_vid])),
  165. ));
  166. $new_header = t('!date by !username', array(
  167. '!date' => l(format_date($new_node->revision_timestamp), "node/$node->nid/revisions/$new_node->vid/view", array('absolute' => 1)),
  168. '!username' => theme('username', array('account' => $node_revisions[$new_vid])),
  169. ));
  170. $old_log = $old_node->log != '' ? '<p class="revision-log">' . filter_xss($old_node->log) . '</p>' : '';
  171. $new_log = $new_node->log != '' ? '<p class="revision-log">' . filter_xss($new_node->log) . '</p>' : '';
  172. // Generate previous diff/next diff links.
  173. $nav_suffix = ($default_state != $state) ? '/' . str_replace('_', '-', $state) : '';
  174. $next_vid = _diff_get_next_vid($node_revisions, $new_vid);
  175. if ($next_vid) {
  176. $next_link = l(t('Next difference >'), 'node/' . $node->nid . '/revisions/view/' . $new_vid . '/' . $next_vid . $nav_suffix, array('absolute' => 1));
  177. }
  178. else {
  179. $next_link = '';
  180. }
  181. $prev_vid = _diff_get_previous_vid($node_revisions, $old_vid);
  182. if ($prev_vid) {
  183. $prev_link = l(t('< Previous difference'), 'node/' . $node->nid . '/revisions/view/' . $prev_vid . '/' . $old_vid . $nav_suffix, array('absolute' => 1));
  184. }
  185. else {
  186. $prev_link = '';
  187. }
  188. $header = _diff_default_header($old_header, $new_header);
  189. $rows = array();
  190. if ($old_log || $new_log) {
  191. $rows['logs'] = array(
  192. array(
  193. 'data' => $old_log,
  194. 'colspan' => 2,
  195. ),
  196. array(
  197. 'data' => $new_log,
  198. 'colspan' => 2,
  199. ),
  200. );
  201. }
  202. $rows['navigation'] = array(
  203. array(
  204. 'data' => $prev_link,
  205. 'class' => array('diff-prevlink'),
  206. 'colspan' => 2,
  207. ),
  208. array(
  209. 'data' => $next_link,
  210. 'class' => array('diff-nextlink'),
  211. 'colspan' => 2,
  212. ),
  213. );
  214. $links = array();
  215. foreach (diff_available_states('node') as $alternative_state => $label) {
  216. if ($alternative_state == $state) {
  217. // @todo: Should we show both or just alternatives?
  218. }
  219. $links[$alternative_state] = array(
  220. 'title' => $label,
  221. 'href' => "node/{$node->nid}/revisions/view/{$old_vid}/{$new_vid}" . ($alternative_state == $default_state ? '' : '/' . str_replace('_', '-', $alternative_state)),
  222. );
  223. }
  224. if (count($links) > 1) {
  225. $state_links = theme('links', array(
  226. 'links' => $links,
  227. 'attributes' => array('class' => array('links', 'inline')),
  228. ));
  229. $rows['states'] = array(
  230. array(
  231. 'data' => $state_links,
  232. 'class' => 'diff-links',
  233. 'colspan' => 4,
  234. ),
  235. );
  236. }
  237. $rows = array_merge($rows, _diff_body_rows($old_node, $new_node, $state));
  238. $build['diff_table'] = array(
  239. '#theme' => 'table__diff__standard',
  240. '#header' => $header,
  241. '#rows' => $rows,
  242. '#attributes' => array('class' => array('diff')),
  243. '#colgroups' => _diff_default_cols(),
  244. '#sticky' => FALSE,
  245. );
  246. // Allow users to hide or set the display mode of the preview.
  247. if (node_is_page($node) && $view_mode = variable_get('diff_view_mode_preview_node_' . $new_node->type, 'full')) {
  248. $header = '';
  249. if ($node->vid == $new_vid) {
  250. $header .= '<div class="diff-section-title">' . t('Current revision:') . '</div>';
  251. }
  252. else {
  253. $header .= '<div class="diff-section-title">' . t('Revision of @new_date:', array('@new_date' => format_date($new_node->revision_timestamp))) . '</div>';
  254. }
  255. $build['diff_preview']['header']['#markup'] = $header;
  256. // Don't include node links or comments when viewing the diff.
  257. $build['diff_preview']['content'] = node_view($new_node, $view_mode);
  258. if (isset($build['diff_preview']['content']['links'])) {
  259. unset($build['diff_preview']['content']['links']);
  260. }
  261. if (isset($build['diff_preview']['content']['comments'])) {
  262. unset($build['diff_preview']['content']['comments']);
  263. }
  264. }
  265. return $build;
  266. }
  267. /**
  268. * Creates an array of rows which represent the difference between nodes.
  269. *
  270. * @param object $old_node
  271. * Node for comparison which will be displayed on the left side.
  272. * @param object $new_node
  273. * Node for comparison which will be displayed on the right side.
  274. * @param boolean $state
  275. * The state to render for the diff.
  276. */
  277. function _diff_body_rows($old_node, $new_node, $state = 'raw') {
  278. // This is an unique index only, so no need for drupal_static().
  279. static $table_row_counter = 0;
  280. if ($theme = variable_get('diff_theme', 'default')) {
  281. drupal_add_css(drupal_get_path('module', 'diff') . "/css/diff.{$theme}.css");
  282. }
  283. module_load_include('inc', 'diff', 'includes/node');
  284. $rows = array();
  285. $any_visible_change = FALSE;
  286. $context = array(
  287. 'entity_type' => 'node',
  288. 'states' => array($state),
  289. 'view_mode' => 'diff_standard',
  290. );
  291. $node_diffs = diff_compare_entities($old_node, $new_node, $context);
  292. // Track line numbers between multiple diffs.
  293. $line_stats = array(
  294. 'counter' => array('x' => 0, 'y' => 0),
  295. 'offset' => array('x' => 0, 'y' => 0),
  296. );
  297. // Render diffs for each.
  298. foreach ($node_diffs as $node_diff) {
  299. $show_header = !empty($node_diff['#name']);
  300. // These are field level settings.
  301. if ($show_header && isset($node_diff['#settings']['show_header'])) {
  302. $show_header = $show_header && $node_diff['#settings']['show_header'];
  303. }
  304. // Line counting and line header options.
  305. if (empty($node_diff['#settings']['line_counter'])) {
  306. $line_counter = FALSE;
  307. }
  308. else {
  309. $line_counter = $node_diff['#settings']['line_counter'];
  310. }
  311. // Every call to 'line' resets the counters.
  312. if ($line_counter) {
  313. $line_stats['counter']['x'] = 0;
  314. $line_stats['counter']['y'] = 0;
  315. if ($line_counter == 'line' && 0) {
  316. $line_stats['offset']['x'] = 0;
  317. $line_stats['offset']['y'] = 0;
  318. }
  319. $line_stats_ref = $line_stats;
  320. }
  321. else {
  322. $line_stats_ref = NULL;
  323. }
  324. list($old, $new) = diff_extract_state($node_diff, $state);
  325. if ($node_diff_rows = diff_get_rows($old, $new, $line_counter && $line_counter != 'hidden', $line_stats_ref)) {
  326. if ($line_counter && $line_counter != 'line') {
  327. $line_stats['offset']['x'] += $line_stats_ref['counter']['x'];
  328. $line_stats['offset']['y'] += $line_stats_ref['counter']['y'];
  329. }
  330. if ($show_header) {
  331. $rows['diff-header-' . $table_row_counter++] = array(
  332. array(
  333. 'data' => t('Changes to %name', array('%name' => $node_diff['#name'])),
  334. 'class' => 'diff-section-title',
  335. 'colspan' => 4,
  336. ),
  337. );
  338. }
  339. // To avoid passing counter to the Diff engine, index rows manually here
  340. // to allow modules to interact with the table. i.e. no array_merge().
  341. foreach ($node_diff_rows as $row) {
  342. $rows['diff-row-' . $table_row_counter++] = $row;
  343. }
  344. $any_visible_change = TRUE;
  345. }
  346. }
  347. if (!$any_visible_change) {
  348. $rows['diff-empty-' . $table_row_counter++] = array(
  349. array(
  350. 'data' => t('No visible changes'),
  351. 'class' => 'diff-section-title',
  352. 'colspan' => 4,
  353. ),
  354. );
  355. // @todo: revise this.
  356. // Needed to keep safari happy.
  357. $rows['diff-empty-' . $table_row_counter++] = array(
  358. array('data' => ''),
  359. array('data' => ''),
  360. array('data' => ''),
  361. array('data' => ''),
  362. );
  363. }
  364. return $rows;
  365. }
  366. /**
  367. * Generic callback to compare two entities.
  368. */
  369. function diff_compare_entities($left_entity, $right_entity, $context) {
  370. $entity_type = $context['entity_type'];
  371. list(, , $bundle) = entity_extract_ids($entity_type, $right_entity);
  372. $context['bundle'] = $bundle;
  373. $context['old_entity'] = $left_entity;
  374. $context['new_entity'] = $right_entity;
  375. $context += array(
  376. 'states' => array('raw'),
  377. 'view_mode' => FALSE,
  378. 'language' => LANGUAGE_NONE,
  379. );
  380. $diff = module_invoke_all('entity_diff', $left_entity, $right_entity, $context);
  381. // Allow other modules to interact directly with the results.
  382. drupal_alter('entity_diff', $diff, $context);
  383. // We start off assuming all form elements are in the correct order.
  384. $diff['#sorted'] = TRUE;
  385. // Field rows. Recurse through all child elements.
  386. $count = 0;
  387. foreach (element_children($diff) as $key) {
  388. if (!isset($diff[$key]['#states'])) {
  389. $diff[$key]['#states'] = array();
  390. }
  391. // Ensure that the element follows the new #states format.
  392. if (isset($diff[$key]['#old'])) {
  393. $diff[$key]['#states']['raw']['#old'] = $diff[$key]['#old'];
  394. unset($diff[$key]['#old']);
  395. }
  396. if (isset($diff[$key]['#new'])) {
  397. $diff[$key]['#states']['raw']['#new'] = $diff[$key]['#new'];
  398. unset($diff[$key]['#new']);
  399. }
  400. // If requested, we can convert the .
  401. foreach (array('raw', 'rendered') as $state) {
  402. if (in_array($state . '_plain', $context['states'])) {
  403. diff_markdown_state($diff[$key], $state);
  404. }
  405. }
  406. // Assign a decimal placeholder weight to preserve original array order.
  407. if (!isset($diff[$key]['#weight'])) {
  408. $diff[$key]['#weight'] = $count / 1000;
  409. }
  410. else {
  411. // If one child element has a weight then we will need to sort later.
  412. unset($diff['#sorted']);
  413. }
  414. $count++;
  415. }
  416. // One of the children has a #weight.
  417. if (!isset($diff['#sorted'])) {
  418. uasort($diff, 'element_sort');
  419. }
  420. // Process the array and get line counts per field.
  421. array_walk($diff, 'diff_process_state_lines');
  422. return $diff;
  423. }
  424. function diff_process_state_lines(&$diff, $key) {
  425. foreach ($diff['#states'] as $state => $data) {
  426. if (isset($data['#old'])) {
  427. if (is_string($data['#old'])) {
  428. $diff['#states'][$state]['#old'] = explode("\n", $data['#old']);
  429. }
  430. $diff['#states'][$state]['#count_old'] = count($diff['#states'][$state]['#old']);
  431. }
  432. else {
  433. $diff['#states'][$state]['#count_old'] = 0;
  434. }
  435. if (isset($data['#new'])) {
  436. if (is_string($data['#new'])) {
  437. $diff['#states'][$state]['#new'] = explode("\n", $data['#new']);
  438. }
  439. $diff['#states'][$state]['#count_new'] = count($diff['#states'][$state]['#new']);
  440. }
  441. else {
  442. $diff['#states'][$state]['#count_new'] = 0;
  443. }
  444. }
  445. }
  446. /**
  447. * Helper function to render plain states from the corresponding raw state.
  448. *
  449. * @param array $diff
  450. * The Diff Engine output array.
  451. * @param string $state
  452. * The state to markdown.
  453. */
  454. function diff_markdown_state(&$diff, $state) {
  455. list($plain_old, $plain_new) = diff_extract_state($diff, $state . '_plain');
  456. list($old, $new) = diff_extract_state($diff, $state);
  457. $markdown = FALSE;
  458. if (isset($diff['#settings']) && !empty($diff['#settings']['markdown'])) {
  459. if (function_exists($diff['#settings']['markdown'])) {
  460. $markdown = $diff['#settings']['markdown'];
  461. }
  462. }
  463. if (!isset($plain_old) && isset($old)) {
  464. if (is_array($old)) {
  465. $diff['#states'][$state . '_plain']['#old'] = $markdown ? array_map($markdown, $old) : $old;
  466. }
  467. else {
  468. $diff['#states'][$state . '_plain']['#old'] = $markdown ? $markdown($old) : $old;
  469. }
  470. }
  471. if (!isset($plain_new) && isset($new)) {
  472. if (is_array($new)) {
  473. $diff['#states'][$state . '_plain']['#new'] = $markdown ? array_map($markdown, $new) : $new;
  474. }
  475. else {
  476. $diff['#states'][$state . '_plain']['#new'] = $markdown ? $markdown($new) : $new;
  477. }
  478. }
  479. }
  480. /**
  481. * Get the entry in the revisions list after $vid.
  482. *
  483. * @param array $node_revisions
  484. * Array of node revision IDs in descending order.
  485. * @param int $vid
  486. * Version ID to look for.
  487. *
  488. * @return boolean|integer
  489. * Returns FALSE if $vid is the last entry.
  490. */
  491. function _diff_get_next_vid($node_revisions, $vid) {
  492. $previous = NULL;
  493. foreach ($node_revisions as $revision) {
  494. if ($revision->vid == $vid) {
  495. return ($previous ? $previous->vid : FALSE);
  496. }
  497. $previous = $revision;
  498. }
  499. return FALSE;
  500. }
  501. /**
  502. * Get the entry in the revision list before $vid.
  503. *
  504. * @param array $node_revisions
  505. * Array of node revision IDs in descending order.
  506. * @param integer $vid
  507. * Version ID to look for.
  508. *
  509. * @return boolean|integer
  510. * Returns FALSE if $vid is the first entry.
  511. */
  512. function _diff_get_previous_vid($node_revisions, $vid) {
  513. $previous = NULL;
  514. foreach ($node_revisions as $revision) {
  515. if ($previous && $previous->vid == $vid) {
  516. return $revision->vid;
  517. }
  518. $previous = $revision;
  519. }
  520. return FALSE;
  521. }
  522. /**
  523. * Helper function to create default 'cols' array for diff table.
  524. */
  525. function _diff_default_cols() {
  526. return array(
  527. array(
  528. array(
  529. 'class' => 'diff-marker',
  530. ),
  531. array(
  532. 'class' => 'diff-content',
  533. ),
  534. array(
  535. 'class' => 'diff-marker',
  536. ),
  537. array(
  538. 'class' => 'diff-content',
  539. ),
  540. ),
  541. );
  542. }
  543. /**
  544. * Helper function to create default 'header' array for diff table.
  545. */
  546. function _diff_default_header($old_header = '', $new_header = '') {
  547. return array(
  548. array(
  549. 'data' => $old_header,
  550. 'colspan' => 2,
  551. ),
  552. array(
  553. 'data' => $new_header,
  554. 'colspan' => 2,
  555. ),
  556. );
  557. }
  558. /**
  559. * Show the inline diff for a given node, vid.
  560. *
  561. * If vid = 0 or no previous vid exists for the given revision returns the
  562. * normally rendered content of the specified revision.
  563. */
  564. function diff_inline_show($node, $vid = 0, $metadata = TRUE) {
  565. $new_node = $vid ? node_load($node->nid, $vid, TRUE) : clone $node;
  566. node_build_content($new_node);
  567. $new = drupal_render($new_node->content);
  568. $old = $vid ? _diff_get_previous_vid(node_revision_list($node), $vid) : 0;
  569. if ($old) {
  570. $old_node = node_load($node->nid, $old, TRUE);
  571. node_build_content($old_node);
  572. $old = drupal_render($old_node->content);
  573. $output = $metadata ? theme('diff_inline_metadata', array('node' => $new_node)) : '';
  574. $output .= diff_get_inline($old, $new);
  575. return $output;
  576. }
  577. return $new;
  578. }