Index: src/wp-includes/post-template.php
===================================================================
--- src/wp-includes/post-template.php	(revision 30812)
+++ src/wp-includes/post-template.php	(working copy)
@@ -999,8 +999,12 @@
  * @param array|string $args {
  *     Array or string of arguments. Optional.
  *
+ *     @type string $after        Content to display after each page link when using the 'custom' format.
+ *                                Default empty.
+ *     @type string $authors      Comma-separated list of author IDs. Default empty (all authors).
+ *     @type string $before       Content to display before each page link when using the 'custom' format.
+ *                                Default empty.
  *     @type int    $child_of     Display only the sub-pages of a single page by ID. Default 0 (all pages).
- *     @type string $authors      Comma-separated list of author IDs. Default empty (all authors).
  *     @type string $date_format  PHP date format to use for the listed pages. Relies on the 'show_date' parameter.
  *                                Default is the value of 'date_format' option.
  *     @type int    $depth        Number of levels in the hierarchy of pages to include in the generated list.
@@ -1008,6 +1012,7 @@
  *                                the given n depth). Default 0.
  *     @type bool   $echo         Whether or not to echo the list of pages. Default true.
  *     @type string $exclude      Comma-separated list of page IDs to exclude. Default empty.
+ *     @type string $format       How to display the pages. Accepts 'list' or 'custom'. Default 'list'.
  *     @type array  $include      Comma-separated list of page IDs to include. Default empty.
  *     @type string $link_after   Text or HTML to follow the page link label. Default null.
  *     @type string $link_before  Text or HTML to precede the page link label. Default null.
@@ -1026,12 +1031,21 @@
  */
 function wp_list_pages( $args = '' ) {
 	$defaults = array(
-		'depth' => 0, 'show_date' => '',
+		'after' => '',
+		'authors' => '',
+		'before' => '',
+		'child_of' => 0,
 		'date_format' => get_option( 'date_format' ),
-		'child_of' => 0, 'exclude' => '',
-		'title_li' => __( 'Pages' ), 'echo' => 1,
-		'authors' => '', 'sort_column' => 'menu_order, post_title',
-		'link_before' => '', 'link_after' => '', 'walker' => '',
+		'depth' => 0,
+		'echo' => 1,
+		'exclude' => '',
+		'format' => 'list',
+		'link_after' => '',
+		'link_before' => '',
+		'show_date' => '',
+		'sort_column' => 'menu_order, post_title',
+		'title_li' => __( 'Pages' ),
+		'walker' => '',
 	);
 
 	$r = wp_parse_args( $args, $defaults );
@@ -1202,7 +1216,7 @@
  */
 function walk_page_tree($pages, $depth, $current_page, $r) {
 	if ( empty($r['walker']) )
-		$walker = new Walker_Page;
+		$walker = 'custom' === $r['format'] ? new Walker_PageCustom : new Walker_Page;
 	else
 		$walker = $r['walker'];
 
@@ -1436,6 +1450,111 @@
 	}
 }
 
+/**
+ * Handle the "custom" format for wp_list_pages().
+ *
+ * @since 4.2
+ * @uses Walker
+ */
+class Walker_PageCustom extends Walker {
+	/**
+	 * @see Walker::$tree_type
+	 * @since 2.1.0
+	 * @var string
+	 */
+	public $tree_type = 'page';
+
+	/**
+	 * @see Walker::$db_fields
+	 * @since 2.1.0
+	 * @todo Decouple this
+	 * @var array
+	 */
+	public $db_fields = array ('parent' => 'post_parent', 'id' => 'ID');
+
+	/**
+	 * @see Walker::start_el()
+	 * @since 2.1.0
+	 *
+	 * @param string $output Passed by reference. Used to append additional content.
+	 * @param object $page Page data object.
+	 * @param int $depth Depth of page. Used for padding.
+	 * @param array $args Additional arguments passed to the walker.
+	 * @param int $current_page Page ID.
+	 */
+	public function start_el( &$output, $page, $depth = 0, $args = array(), $current_page = 0 ) {
+		$css_class = array( 'page_item', 'page-item-' . $page->ID );
+
+		if ( isset( $args['pages_with_children'][ $page->ID ] ) ) {
+			$css_class[] = 'page_item_has_children';
+		}
+
+		if ( ! empty( $current_page ) ) {
+			$_current_page = get_post( $current_page );
+			if ( $_current_page && in_array( $page->ID, $_current_page->ancestors ) ) {
+				$css_class[] = 'current_page_ancestor';
+			}
+			if ( $page->ID == $current_page ) {
+				$css_class[] = 'current_page_item';
+			} elseif ( $_current_page && $page->ID == $_current_page->post_parent ) {
+				$css_class[] = 'current_page_parent';
+			}
+		} elseif ( $page->ID == get_option('page_for_posts') ) {
+			$css_class[] = 'current_page_parent';
+		}
+
+		/**
+		 * Filter the list of CSS classes to include with each page item in the list.
+		 *
+		 * @since 2.8.0
+		 *
+		 * @see wp_list_pages()
+		 *
+		 * @param array   $css_class    An array of CSS classes to be applied
+		 *                             to each list item.
+		 * @param WP_Post $page         Page data object.
+		 * @param int     $depth        Depth of page, used for padding.
+		 * @param array   $args         An array of arguments.
+		 * @param int     $current_page ID of the current page.
+		 */
+		$css_classes = implode( ' ', apply_filters( 'page_css_class', $css_class, $page, $depth, $args, $current_page ) );
+
+		if ( '' === $page->post_title ) {
+			$page->post_title = sprintf( __( '#%d (no title)' ), $page->ID );
+		}
+
+		$args['before'] = empty( $args['before'] ) ? '' : $args['before'];
+		$args['link_before'] = empty( $args['link_before'] ) ? '' : $args['link_before'];
+		$args['link_after'] = empty( $args['link_after'] ) ? '' : $args['link_after'];
+		$args['after'] = empty( $args['after'] ) ? '' : $args['after'];
+
+		/** This filter is documented in wp-includes/post-template.php */
+		$output .= sprintf(
+			"\n" . $args['before'],
+			$css_classes
+		);
+		$output .= sprintf(
+			'<a href="%s">%s%s%s</a>',
+			get_permalink( $page->ID ),
+			$args['link_before'],
+			apply_filters( 'the_title', $page->post_title, $page->ID ),
+			$args['link_after']
+		);
+
+		if ( ! empty( $args['show_date'] ) ) {
+			if ( 'modified' == $args['show_date'] ) {
+				$time = $page->post_modified;
+			} else {
+				$time = $page->post_date;
+			}
+
+			$date_format = empty( $args['date_format'] ) ? '' : $args['date_format'];
+			$output .= " " . mysql2date( $date_format, $time );
+		}
+		$output .= $args['after'];
+	}
+}
+
 //
 // Attachments
 //
Index: tests/phpunit/tests/post/listPages.php
===================================================================
--- tests/phpunit/tests/post/listPages.php	(revision 30812)
+++ tests/phpunit/tests/post/listPages.php	(working copy)
@@ -20,6 +20,9 @@
 		'include'      => '',
 		'post_type'    => 'page',
 		'post_status'  => 'publish',
+		'format' => ''
+		'after' => '',
+		'before' => '',
 	);
 	*/
 	function setUp() {
@@ -342,4 +345,72 @@
 		$actual = wp_list_pages( $args );
 		$this->AssertEquals( $expected['exclude'], $actual );
 	}
+
+	function test_wp_list_pages_format() {
+		$args = array(
+			'echo' => false,
+			'format' => 'custom'
+		);
+		$expected['format'] = '<li class="pagenav">Pages<ul>
+<a href="' . get_permalink( 1 ) . '">Parent 1</a>
+<a href="' . get_permalink( 4 ) . '">Child 1</a>
+<a href="' . get_permalink( 5 ) . '">Child 2</a>
+<a href="' . get_permalink( 6 ) . '">Child 3</a>
+<a href="' . get_permalink( 2 ) . '">Parent 2</a>
+<a href="' . get_permalink( 7 ) . '">Child 1</a>
+<a href="' . get_permalink( 8 ) . '">Child 2</a>
+<a href="' . get_permalink( 9 ) . '">Child 3</a>
+<a href="' . get_permalink( 3 ) . '">Parent 3</a>
+<a href="' . get_permalink( 10 ) . '">Child 1</a>
+<a href="' . get_permalink( 11 ) . '">Child 2</a>
+<a href="' . get_permalink( 12 ) . '">Child 3</a></ul></li>';
+		$actual = wp_list_pages( $args );
+		$this->AssertEquals( $expected['format'], $actual );
+	}
+
+	function test_wp_list_pages_before() {
+		$args = array(
+			'echo' => false,
+			'format' => 'custom',
+			'before' => 'BEFORE'
+		);
+		$expected['before'] = '<li class="pagenav">Pages<ul>
+BEFORE<a href="' . get_permalink( 1 ) . '">Parent 1</a>
+BEFORE<a href="' . get_permalink( 4 ) . '">Child 1</a>
+BEFORE<a href="' . get_permalink( 5 ) . '">Child 2</a>
+BEFORE<a href="' . get_permalink( 6 ) . '">Child 3</a>
+BEFORE<a href="' . get_permalink( 2 ) . '">Parent 2</a>
+BEFORE<a href="' . get_permalink( 7 ) . '">Child 1</a>
+BEFORE<a href="' . get_permalink( 8 ) . '">Child 2</a>
+BEFORE<a href="' . get_permalink( 9 ) . '">Child 3</a>
+BEFORE<a href="' . get_permalink( 3 ) . '">Parent 3</a>
+BEFORE<a href="' . get_permalink( 10 ) . '">Child 1</a>
+BEFORE<a href="' . get_permalink( 11 ) . '">Child 2</a>
+BEFORE<a href="' . get_permalink( 12 ) . '">Child 3</a></ul></li>';
+		$actual = wp_list_pages( $args );
+		$this->AssertEquals( $expected['before'], $actual );
+	}
+
+	function test_wp_list_pages_after() {
+		$args = array(
+			'echo' => false,
+			'format' => 'custom',
+			'after' => 'AFTER'
+		);
+		$expected['after'] = '<li class="pagenav">Pages<ul>
+<a href="' . get_permalink( 1 ) . '">Parent 1</a>AFTER
+<a href="' . get_permalink( 4 ) . '">Child 1</a>AFTER
+<a href="' . get_permalink( 5 ) . '">Child 2</a>AFTER
+<a href="' . get_permalink( 6 ) . '">Child 3</a>AFTER
+<a href="' . get_permalink( 2 ) . '">Parent 2</a>AFTER
+<a href="' . get_permalink( 7 ) . '">Child 1</a>AFTER
+<a href="' . get_permalink( 8 ) . '">Child 2</a>AFTER
+<a href="' . get_permalink( 9 ) . '">Child 3</a>AFTER
+<a href="' . get_permalink( 3 ) . '">Parent 3</a>AFTER
+<a href="' . get_permalink( 10 ) . '">Child 1</a>AFTER
+<a href="' . get_permalink( 11 ) . '">Child 2</a>AFTER
+<a href="' . get_permalink( 12 ) . '">Child 3</a>AFTER</ul></li>';
+		$actual = wp_list_pages( $args );
+		$this->AssertEquals( $expected['after'], $actual );
+	}
 }
Index: tests/phpunit/tests/post/pageMenu.php
===================================================================
--- tests/phpunit/tests/post/pageMenu.php	(revision 0)
+++ tests/phpunit/tests/post/pageMenu.php	(working copy)
@@ -0,0 +1,140 @@
+<?php
+
+class Tests_Page_Menu extends WP_UnitTestCase {
+	var $pages;
+
+	/*
+	$defaults = array(
+		'depth' => 0,
+		'show_date' => '',
+		'date_format' => get_option('date_format'),
+		'child_of' => 0,
+		'exclude' => '',
+		'title_li' => __('Pages'),
+		'echo' => 1,
+		'authors' => '',
+		'sort_column' => 'menu_order, post_title',
+		'link_before' => '',
+		'link_after' => '',
+		'walker' => '',
+		'include'      => '',
+		'post_type'    => 'page',
+		'post_status'  => 'publish',
+		'format' => ''
+		'after' => '',
+		'before' => '',
+	);
+	*/
+	function setUp() {
+		parent::setUp();
+		global $wpdb;
+		$wpdb->query( 'TRUNCATE ' . $wpdb->prefix . 'posts' );
+		$pages = array();
+		$this->factory->user->create();
+		$pages[] = $this->factory->post->create( array( 'post_type' => 'page', 'post_title' => 'Parent 1' ) );
+		$pages[] = $this->factory->post->create( array( 'post_type' => 'page', 'post_title' => 'Parent 2' ) );
+		$pages[] = $this->factory->post->create( array( 'post_type' => 'page', 'post_title' => 'Parent 3', 'post_author' => '2' ) );
+
+		foreach ( $pages as $page ) {
+			$this->pages[$page] = $this->factory->post->create( array( 'post_parent' => $page, 'post_type' => 'page', 'post_title' => 'Child 1' ) );
+			$this->pages[$page] = $this->factory->post->create( array( 'post_parent' => $page, 'post_type' => 'page', 'post_title' => 'Child 2' ) );
+			$this->pages[$page] = $this->factory->post->create( array( 'post_parent' => $page, 'post_type' => 'page', 'post_title' => 'Child 3' ) );
+		}
+	}
+
+	function test_wp_page_menu_default() {
+		$args = array(
+			'echo' => false
+		);
+		$expected['default'] = '<div class="menu"><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></div>';
+		$actual = wp_page_menu( $args );
+		$this->AssertEquals( $this->collapse_whitespace( $expected['default'] ), $actual );
+	}
+
+	function test_wp_page_menu_echo() {
+		$args = array(
+			'echo' => true,
+			'depth' => 1
+		);
+		$expected['echo'] = '<div class="menu"><ul>
+<li class="page_item page-item-1 page_item_has_children"><a href="' . get_permalink( 1 ) . '">Parent 1</a></li>
+<li class="page_item page-item-2 page_item_has_children"><a href="' . get_permalink( 2 ) . '">Parent 2</a></li>
+<li class="page_item page-item-3 page_item_has_children"><a href="' . get_permalink( 3 ) . '">Parent 3</a></li>
+</ul></div>';
+		ob_start();
+		wp_page_menu( $args );
+		$actual = ob_get_clean();
+		$this->AssertEquals( $this->collapse_whitespace( $expected['echo'] ), $actual );
+	}
+
+	function test_wp_page_menu_show_home() {
+		$args = array(
+			'echo' => false,
+			'depth' => 1,
+			'show_home' => 1
+		);
+		$expected['show_home'] = '<div class="menu"><ul>
+<li ><a href="' . home_url( '/' ) . '">' . __( 'Home' ) . '</a></li>
+<li class="page_item page-item-1 page_item_has_children"><a href="' . get_permalink( 1 ) . '">Parent 1</a></li>
+<li class="page_item page-item-2 page_item_has_children"><a href="' . get_permalink( 2 ) . '">Parent 2</a></li>
+<li class="page_item page-item-3 page_item_has_children"><a href="' . get_permalink( 3 ) . '">Parent 3</a></li>
+</ul></div>';
+		$actual = wp_page_menu( $args );
+		$this->AssertEquals( $this->collapse_whitespace( $expected['show_home'] ), $actual );
+
+		// An integer *or* a boolean TRUE should cause the default home link
+		$args['show_home'] = true;
+		$actual = wp_page_menu( $args );
+		$this->AssertEquals( $this->collapse_whitespace( $expected['show_home'] ), $actual );
+	}
+
+	function test_wp_page_menu_show_home_custom() {
+		$args = array(
+			'echo' => false,
+			'depth' => 1,
+			'show_home' => 'HOMEPAGE'
+		);
+		$expected['show_home_custom'] = '<div class="menu"><ul>
+<li ><a href="' . home_url( '/' ) . '">HOMEPAGE</a></li>
+<li class="page_item page-item-1 page_item_has_children"><a href="' . get_permalink( 1 ) . '">Parent 1</a></li>
+<li class="page_item page-item-2 page_item_has_children"><a href="' . get_permalink( 2 ) . '">Parent 2</a></li>
+<li class="page_item page-item-3 page_item_has_children"><a href="' . get_permalink( 3 ) . '">Parent 3</a></li>
+</ul></div>';
+		$actual = wp_page_menu( $args );
+		$this->AssertEquals( $this->collapse_whitespace( $expected['show_home_custom'] ), $actual );
+	}
+
+	/**
+	 * wp_page_menu() does some string replacement just before returning that collapses whitespace
+	 * characters. For the sake of our assertions, we'll run that same operation.
+	 *
+	 * @param str $expected Your expected output, with whitespace for readability.
+	 * @return str The collapsed expectation.
+	 */
+  protected function collapse_whitespace( $expected ) {
+  	return str_replace( array( "\r", "\n", "\t" ), '', $expected ) . "\n";
+  }
+
+}

Property changes on: tests/phpunit/tests/post/pageMenu.php
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
