From 0c88ec217d73c2155dfc215b30b908e44224ddf7 Mon Sep 17 00:00:00 2001
From: Peter Wilson <peterwilsoncc@git.wordpress.org>
Date: Tue, 6 Sep 2016 09:05:45 +0000
Subject: [PATCH] Menus: Add white space option to `wp_nav_menu()` and
 `wp_list_pages()`.

Adds an `item_spacing` option to the arguments array for the functions `wp_nav_menu()`, `wp_list_pages()`, and `wp_page_menu()`. `item_spacing` is a boolean accepting either `preserve` or `discard`.

Previously, certain CSS choices could result in a site's layout changing if `wp_nav_menu()` fell back to the default `wp_list_pages()` due to differences in the whitespace within the HTML. The new argument ensures a function outputs consistant HTML while maintaining backward compatibility.

Fixes #35206.


git-svn-id: https://develop.svn.wordpress.org/trunk@38523 602fd350-edb4-49c9-b593-d223f7449a82
---
 src/wp-includes/class-walker-nav-menu.php | 40 ++++++++++--
 src/wp-includes/class-walker-page.php     | 40 ++++++++++--
 src/wp-includes/nav-menu-template.php     |  9 ++-
 src/wp-includes/post-template.php         | 77 ++++++++++++++---------
 tests/phpunit/tests/post/listPages.php    | 11 ++++
 tests/phpunit/tests/post/nav-menu.php     | 62 ++++++++++++++++++
 tests/phpunit/tests/post/template.php     | 15 +++++
 7 files changed, 213 insertions(+), 41 deletions(-)

diff --git a/src/wp-includes/class-walker-nav-menu.php b/src/wp-includes/class-walker-nav-menu.php
index 43eaa91c66..edc6f2a607 100644
--- a/src/wp-includes/class-walker-nav-menu.php
+++ b/src/wp-includes/class-walker-nav-menu.php
@@ -50,8 +50,15 @@ class Walker_Nav_Menu extends Walker {
 	 * @param array  $args   An array of wp_nav_menu() arguments.
 	 */
 	public function start_lvl( &$output, $depth = 0, $args = array() ) {
-		$indent = str_repeat("\t", $depth);
-		$output .= "\n$indent<ul class=\"sub-menu\">\n";
+		if ( 'preserve' === $args->item_spacing ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$indent = str_repeat( $t, $depth );
+		$output .= "{$n}{$indent}<ul class=\"sub-menu\">{$n}";
 	}
 
 	/**
@@ -66,8 +73,15 @@ class Walker_Nav_Menu extends Walker {
 	 * @param array  $args   An array of wp_nav_menu() arguments.
 	 */
 	public function end_lvl( &$output, $depth = 0, $args = array() ) {
-		$indent = str_repeat("\t", $depth);
-		$output .= "$indent</ul>\n";
+		if ( 'preserve' === $args->item_spacing ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$indent = str_repeat( $t, $depth );
+		$output .= "$indent</ul>{$n}";
 	}
 
 	/**
@@ -85,7 +99,14 @@ class Walker_Nav_Menu extends Walker {
 	 * @param int    $id     Current item ID.
 	 */
 	public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
-		$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
+		if ( 'preserve' === $args->item_spacing ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$indent = ( $depth ) ? str_repeat( $t, $depth ) : '';
 
 		$classes = empty( $item->classes ) ? array() : (array) $item->classes;
 		$classes[] = 'menu-item-' . $item->ID;
@@ -216,7 +237,14 @@ class Walker_Nav_Menu extends Walker {
 	 * @param array  $args   An array of wp_nav_menu() arguments.
 	 */
 	public function end_el( &$output, $item, $depth = 0, $args = array() ) {
-		$output .= "</li>\n";
+		if ( 'preserve' === $args->item_spacing ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$output .= "</li>{$n}";
 	}
 
 } // Walker_Nav_Menu
diff --git a/src/wp-includes/class-walker-page.php b/src/wp-includes/class-walker-page.php
index fd55ccee4d..d2ad1694c7 100644
--- a/src/wp-includes/class-walker-page.php
+++ b/src/wp-includes/class-walker-page.php
@@ -53,8 +53,15 @@ class Walker_Page extends Walker {
 	 *                       Default empty array.
 	 */
 	public function start_lvl( &$output, $depth = 0, $args = array() ) {
-		$indent = str_repeat("\t", $depth);
-		$output .= "\n$indent<ul class='children'>\n";
+		if ( 'preserve' === $args['item_spacing'] ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$indent = str_repeat( $t, $depth );
+		$output .= "{$n}{$indent}<ul class='children'>{$n}";
 	}
 
 	/**
@@ -71,8 +78,15 @@ class Walker_Page extends Walker {
 	 *                       Default empty array.
 	 */
 	public function end_lvl( &$output, $depth = 0, $args = array() ) {
-		$indent = str_repeat("\t", $depth);
-		$output .= "$indent</ul>\n";
+		if ( 'preserve' === $args['item_spacing'] ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$indent = str_repeat( $t, $depth );
+		$output .= "{$indent}</ul>{$n}";
 	}
 
 	/**
@@ -89,8 +103,15 @@ class Walker_Page extends Walker {
 	 * @param int     $current_page Optional. Page ID. Default 0.
 	 */
 	public function start_el( &$output, $page, $depth = 0, $args = array(), $current_page = 0 ) {
+		if ( 'preserve' === $args['item_spacing'] ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
 		if ( $depth ) {
-			$indent = str_repeat( "\t", $depth );
+			$indent = str_repeat( $t, $depth );
 		} else {
 			$indent = '';
 		}
@@ -175,7 +196,14 @@ class Walker_Page extends Walker {
 	 * @param array   $args   Optional. Array of arguments. Default empty array.
 	 */
 	public function end_el( &$output, $page, $depth = 0, $args = array() ) {
-		$output .= "</li>\n";
+		if ( 'preserve' === $args['item_spacing'] ) {
+			$t = "\t";
+			$n = "\n";
+		} else {
+			$t = '';
+			$n = '';
+		}
+		$output .= "</li>{$n}";
 	}
 
 }
diff --git a/src/wp-includes/nav-menu-template.php b/src/wp-includes/nav-menu-template.php
index 47e7039430..fc62c94a91 100644
--- a/src/wp-includes/nav-menu-template.php
+++ b/src/wp-includes/nav-menu-template.php
@@ -40,6 +40,7 @@ require_once ABSPATH . WPINC . '/class-walker-nav-menu.php';
  *                                               in order to be selectable by the user.
  *     @type string             $items_wrap      How the list items should be wrapped. Default is a ul with an id and class.
  *                                               Uses printf() format with numbered placeholders.
+ *     @type string             $item_spacing    Whether whitespace format the menu's HTML: 'discard' or 'preserve' (default).
  * }
  * @return object|false|void Menu output if $echo is false, false if there are no items or no menu was found.
  */
@@ -47,10 +48,16 @@ function wp_nav_menu( $args = array() ) {
 	static $menu_id_slugs = array();
 
 	$defaults = array( 'menu' => '', 'container' => 'div', 'container_class' => '', 'container_id' => '', 'menu_class' => 'menu', 'menu_id' => '',
-	'echo' => true, 'fallback_cb' => 'wp_page_menu', 'before' => '', 'after' => '', 'link_before' => '', 'link_after' => '', 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>',
+	'echo' => true, 'fallback_cb' => 'wp_page_menu', 'before' => '', 'after' => '', 'link_before' => '', 'link_after' => '', 'items_wrap' => '<ul id="%1$s" class="%2$s">%3$s</ul>', 'item_spacing' => 'preserve',
 	'depth' => 0, 'walker' => '', 'theme_location' => '' );
 
 	$args = wp_parse_args( $args, $defaults );
+
+	if ( ! in_array( $args['item_spacing'], array( 'preserve', 'discard' ), true ) ) {
+		// invalid value, fall back to default.
+		$args['item_spacing'] = $defaults['item_spacing'];
+	}
+
 	/**
 	 * Filters the arguments used to display a navigation menu.
 	 *
diff --git a/src/wp-includes/post-template.php b/src/wp-includes/post-template.php
index 7994f89c87..64732d910b 100644
--- a/src/wp-includes/post-template.php
+++ b/src/wp-includes/post-template.php
@@ -1138,6 +1138,7 @@ function wp_dropdown_pages( $args = '' ) {
  *                                'menu_order', 'post_parent', 'ID', 'rand', or 'comment_count'. Default 'post_title'.
  *     @type string $title_li     List heading. Passing a null or empty value will result in no heading, and the list
  *                                will not be wrapped with unordered list `<ul>` tags. Default 'Pages'.
+ *     @type string $item_spacing Whether whitespace format the menu's HTML: 'discard' or 'preserve' (default).
  *     @type Walker $walker       Walker instance to use for listing pages. Default empty (Walker_Page).
  * }
  * @return string|void HTML list of pages.
@@ -1149,11 +1150,16 @@ function wp_list_pages( $args = '' ) {
 		'child_of' => 0, 'exclude' => '',
 		'title_li' => __( 'Pages' ), 'echo' => 1,
 		'authors' => '', 'sort_column' => 'menu_order, post_title',
-		'link_before' => '', 'link_after' => '', 'walker' => '',
+		'link_before' => '', 'link_after' => '', 'item_spacing' => 'preserve', 'walker' => '',
 	);
 
 	$r = wp_parse_args( $args, $defaults );
 
+	if ( ! in_array( $r['item_spacing'], array( 'preserve', 'discard' ), true ) ) {
+		// invalid value, fall back to default.
+		$r['item_spacing'] = $defaults['item_spacing'];
+	}
+
 	$output = '';
 	$current_page = 0;
 
@@ -1230,38 +1236,53 @@ function wp_list_pages( $args = '' ) {
  * @param array|string $args {
  *     Optional. Arguments to generate a page menu. See wp_list_pages() for additional arguments.
  *
- *     @type string          $sort_column How to short the list of pages. Accepts post column names.
- *                                        Default 'menu_order, post_title'.
- *     @type string          $menu_id     ID for the div containing the page list. Default is empty string.
- *     @type string          $menu_class  Class to use for the element containing the page list. Default 'menu'.
- *     @type string          $container   Element to use for the element containing the page list. Default 'div'.
- *     @type bool            $echo        Whether to echo the list or return it. Accepts true (echo) or false (return).
- *                                        Default true.
- *     @type int|bool|string $show_home   Whether to display the link to the home page. Can just enter the text
- *                                        you'd like shown for the home link. 1|true defaults to 'Home'.
- *     @type string          $link_before The HTML or text to prepend to $show_home text. Default empty.
- *     @type string          $link_after  The HTML or text to append to $show_home text. Default empty.
- *     @type string          $before      The HTML or text to prepend to the menu. Default is '<ul>'.
- *     @type string          $after       The HTML or text to append to the menu. Default is '</ul>'.
- *     @type Walker          $walker      Walker instance to use for listing pages. Default empty (Walker_Page).
+ *     @type string          $sort_column  How to short the list of pages. Accepts post column names.
+ *                                         Default 'menu_order, post_title'.
+ *     @type string          $menu_id      ID for the div containing the page list. Default is empty string.
+ *     @type string          $menu_class   Class to use for the element containing the page list. Default 'menu'.
+ *     @type string          $container    Element to use for the element containing the page list. Default 'div'.
+ *     @type bool            $echo         Whether to echo the list or return it. Accepts true (echo) or false (return).
+ *                                         Default true.
+ *     @type int|bool|string $show_home    Whether to display the link to the home page. Can just enter the text
+ *                                         you'd like shown for the home link. 1|true defaults to 'Home'.
+ *     @type string          $link_before  The HTML or text to prepend to $show_home text. Default empty.
+ *     @type string          $link_after   The HTML or text to append to $show_home text. Default empty.
+ *     @type string          $before       The HTML or text to prepend to the menu. Default is '<ul>'.
+ *     @type string          $after        The HTML or text to append to the menu. Default is '</ul>'.
+ *     @type string          $item_spacing Whether whitespace format the menu's HTML: 'discard' or 'preserve' (default).
+ *     @type Walker          $walker       Walker instance to use for listing pages. Default empty (Walker_Page).
  * }
  * @return string|void HTML menu
  */
 function wp_page_menu( $args = array() ) {
 	$defaults = array(
-		'sort_column' => 'menu_order, post_title',
-		'menu_id'     => '',
-		'menu_class'  => 'menu',
-		'container'   => 'div',
-		'echo'        => true,
-		'link_before' => '',
-		'link_after'  => '',
-		'before'      => '<ul>',
-		'after'       => '</ul>',
-		'walker'      => '',
+		'sort_column'  => 'menu_order, post_title',
+		'menu_id'      => '',
+		'menu_class'   => 'menu',
+		'container'    => 'div',
+		'echo'         => true,
+		'link_before'  => '',
+		'link_after'   => '',
+		'before'       => '<ul>',
+		'after'        => '</ul>',
+		'item_spacing' => 'discard',
+		'walker'       => '',
 	);
 	$args = wp_parse_args( $args, $defaults );
 
+	if ( ! in_array( $args['item_spacing'], array( 'preserve', 'discard' ) ) ) {
+		// invalid value, fall back to default.
+		$args['item_spacing'] = $defaults['item_spacing'];
+	}
+
+	if ( 'preserve' === $args['item_spacing'] ) {
+		$t = "\t";
+		$n = "\n";
+	} else {
+		$t = '';
+		$n = '';
+	}
+
 	/**
 	 * Filters the arguments used to generate a page-based menu.
 	 *
@@ -1300,7 +1321,7 @@ function wp_page_menu( $args = array() ) {
 
 	$list_args['echo'] = false;
 	$list_args['title_li'] = '';
-	$menu .= str_replace( array( "\r", "\n", "\t" ), '', wp_list_pages($list_args) );
+	$menu .= wp_list_pages( $list_args );
 
 	$container = sanitize_text_field( $args['container'] );
 
@@ -1315,7 +1336,7 @@ function wp_page_menu( $args = array() ) {
 		if ( isset( $args['fallback_cb'] ) &&
 			'wp_page_menu' === $args['fallback_cb'] &&
 			'ul' !== $container ) {
-			$args['before'] = '<ul>';
+			$args['before'] = "<ul>{$n}";
 			$args['after'] = '</ul>';
 		}
 
@@ -1331,7 +1352,7 @@ function wp_page_menu( $args = array() ) {
 		$attrs .= ' class="' . esc_attr( $args['menu_class'] ) . '"';
 	}
 
-	$menu = "<{$container}{$attrs}>" . $menu . "</{$container}>\n";
+	$menu = "<{$container}{$attrs}>" . $menu . "</{$container}>{$n}";
 
 	/**
 	 * Filters the HTML output of a page-based menu.
diff --git a/tests/phpunit/tests/post/listPages.php b/tests/phpunit/tests/post/listPages.php
index 400d106ae4..d2146312dd 100644
--- a/tests/phpunit/tests/post/listPages.php
+++ b/tests/phpunit/tests/post/listPages.php
@@ -17,6 +17,7 @@ class Tests_List_Pages extends WP_UnitTestCase {
 		'link_before' => '',
 		'link_after' => '',
 		'walker' => '',
+		'item_spacing' => 'preserve',
 		'include'      => '',
 		'post_type'    => 'page',
 		'post_status'  => 'publish',
@@ -342,4 +343,14 @@ class Tests_List_Pages extends WP_UnitTestCase {
 		$actual = wp_list_pages( $args );
 		$this->AssertEquals( $expected['exclude'], $actual );
 	}
+
+	function test_wp_list_pages_discarded_whitespace() {
+		$args = array(
+			'echo' => false,
+			'item_spacing' => 'discard',
+		);
+		$expected['default'] = '<li class="pagenav">Pages<ul><li class="page_item page-item-1 page_item_has_children"><a href="' . get_permalink( 1 ) . '">Parent 1</a><ul class=\'children\'><li class="page_item page-item-4"><a href="' . get_permalink( 4 ) . '">Child 1</a></li><li class="page_item page-item-5"><a href="' . get_permalink( 5 ) . '">Child 2</a></li><li class="page_item page-item-6"><a href="' . get_permalink( 6 ) . '">Child 3</a></li></ul></li><li class="page_item page-item-2 page_item_has_children"><a href="' . get_permalink( 2 ) . '">Parent 2</a><ul class=\'children\'><li class="page_item page-item-7"><a href="' . get_permalink( 7 ) . '">Child 1</a></li><li class="page_item page-item-8"><a href="' . get_permalink( 8 ) . '">Child 2</a></li><li class="page_item page-item-9"><a href="' . get_permalink( 9 ) . '">Child 3</a></li></ul></li><li class="page_item page-item-3 page_item_has_children"><a href="' . get_permalink( 3 ) . '">Parent 3</a><ul class=\'children\'><li class="page_item page-item-10"><a href="' . get_permalink( 10 ) . '">Child 1</a></li><li class="page_item page-item-11"><a href="' . get_permalink( 11 ) . '">Child 2</a></li><li class="page_item page-item-12"><a href="' . get_permalink( 12 ) . '">Child 3</a></li></ul></li></ul></li>';
+		$actual = wp_list_pages( $args );
+		$this->AssertEquals( $expected['default'], $actual );
+	}
 }
diff --git a/tests/phpunit/tests/post/nav-menu.php b/tests/phpunit/tests/post/nav-menu.php
index 83272e7c66..bbcbbabbdf 100644
--- a/tests/phpunit/tests/post/nav-menu.php
+++ b/tests/phpunit/tests/post/nav-menu.php
@@ -277,6 +277,68 @@ class Test_Nav_Menus extends WP_UnitTestCase {
 	}
 
 	/**
+	 * @ticket 35206
+	 */
+	function test_wp_nav_menu_whitespace_options() {
+		$post_id1 = self::factory()->post->create();
+		$post_id2 = self::factory()->post->create();
+		$post_id3 = self::factory()->post->create();
+		$post_id4 = self::factory()->post->create();
+
+		$post_insert = wp_update_nav_menu_item( $this->menu_id, 0, array(
+			'menu-item-type' => 'post_type',
+			'menu-item-object' => 'post',
+			'menu-item-object-id' => $post_id1,
+			'menu-item-status' => 'publish'
+		) );
+
+		$post_inser2 = wp_update_nav_menu_item( $this->menu_id, 0, array(
+			'menu-item-type' => 'post_type',
+			'menu-item-object' => 'post',
+			'menu-item-object-id' => $post_id2,
+			'menu-item-status' => 'publish'
+		) );
+
+		$post_insert3 = wp_update_nav_menu_item( $this->menu_id, 0, array(
+			'menu-item-type' => 'post_type',
+			'menu-item-object' => 'post',
+			'menu-item-parent-id' => $post_insert,
+			'menu-item-object-id' => $post_id3,
+			'menu-item-status' => 'publish'
+		) );
+
+		$post_insert4 = wp_update_nav_menu_item( $this->menu_id, 0, array(
+			'menu-item-type' => 'post_type',
+			'menu-item-object' => 'post',
+			'menu-item-parent-id' => $post_insert,
+			'menu-item-object-id' => $post_id4,
+			'menu-item-status' => 'publish'
+		) );
+
+		// No whitespace suppression.
+		$menu = wp_nav_menu( array(
+			'echo' => false,
+			'menu' => $this->menu_id,
+		) );
+
+		// The markup should include whitespace between <li>s
+		$this->assertRegExp( '/\s<li.*>|<\/li>\s/U', $menu );
+		$this->assertNotRegExp( '/<\/li><li.*>/U', $menu );
+
+
+		// Whitepsace suppressed.
+		$menu = wp_nav_menu( array(
+			'echo'         => false,
+			'item_spacing' => 'discard',
+			'menu'         => $this->menu_id,
+		) );
+
+		// The markup should not include whitespace around <li>s
+		$this->assertNotRegExp( '/\s<li.*>|<\/li>\s/U', $menu );
+		$this->assertRegExp( '/><li.*>|<\/li></U', $menu );
+	}
+
+	/*
 	 * Confirm `wp_nav_menu()` and `Walker_Nav_Menu` passes an $args object to filters.
 	 *
 	 * `wp_nav_menu()` is unique in that it uses an $args object rather than an array.
diff --git a/tests/phpunit/tests/post/template.php b/tests/phpunit/tests/post/template.php
index 0edf60752d..ac5958d7bf 100644
--- a/tests/phpunit/tests/post/template.php
+++ b/tests/phpunit/tests/post/template.php
@@ -299,6 +299,10 @@ NO;
 		// After falling back, the 'after' argument should be set and output as '</ul>'.
 		$this->assertRegExp( '/<\/ul><\/div>/', $menu );
 
+		// After falling back, the markup should include whitespace around <li>s
+		$this->assertRegExp( '/\s<li.*>|<\/li>\s/U', $menu );
+		$this->assertNotRegExp( '/><li.*>|<\/li></U', $menu );
+
 		// No menus + wp_nav_menu() falls back to wp_page_menu(), this time without a container.
 		$menu = wp_nav_menu( array(
 			'echo'      => false,
@@ -307,5 +311,16 @@ NO;
 
 		// After falling back, the empty 'container' argument should still return a container element.
 		$this->assertRegExp( '/<div class="menu">/', $menu );
+
+		// No menus + wp_nav_menu() falls back to wp_page_menu(), this time without white-space.
+		$menu = wp_nav_menu( array(
+			'echo'         => false,
+			'item_spacing' => 'discard',
+		) );
+
+		// After falling back, the markup should not include whitespace around <li>s
+		$this->assertNotRegExp( '/\s<li.*>|<\/li>\s/U', $menu );
+		$this->assertRegExp( '/><li.*>|<\/li></U', $menu );
+
 	}
 }