? drupal_menu_paths.patch
? misc/sample.jpg
? misc/sample.png
Index: includes/menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.328
diff -u -p -r1.328 menu.inc
--- includes/menu.inc	10 Jun 2009 21:52:36 -0000	1.328
+++ includes/menu.inc	6 Jul 2009 04:17:07 -0000
@@ -370,6 +370,7 @@ function menu_get_item($path = NULL, $ro
       ->execute()->fetchAssoc();
     if ($router_item) {
       $map = _menu_translate($router_item, $original_map);
+      $router_item['original_map'] = $original_map;
       if ($map === FALSE) {
         $router_items[$path] = FALSE;
         return FALSE;
@@ -625,13 +626,7 @@ function _menu_translate(&$router_item, 
   }
 
   // Generate the link path for the page request or local tasks.
-  $link_map = explode('/', $router_item['path']);
-  for ($i = 0; $i < $router_item['number_parts']; $i++) {
-    if ($link_map[$i] == '%') {
-      $link_map[$i] = $path_map[$i];
-    }
-  }
-  $router_item['href'] = implode('/', $link_map);
+  $router_item['href'] = _menu_link_path_map_replace($router_item['path'], $path_map);
   $router_item['options'] = array();
   _menu_check_access($router_item, $map);
 
@@ -698,7 +693,14 @@ function _menu_link_translate(&$item) {
     $item['localized_options'] = $item['options'];
   }
   else {
-    $map = explode('/', $item['link_path']);
+    // Replace wildcards in the active trail map using the current path.
+    if (!empty($item['in_active_trail']) && isset($item['active_map'])) {
+      $map = explode('/', _menu_link_path_map_replace($item['link_path'], $item['active_map']));
+    }
+    else {
+      $map = explode('/', $item['link_path']);
+    }
+
     _menu_link_map_translate($map, $item['to_arg_functions']);
     $item['href'] = implode('/', $map);
 
@@ -733,6 +735,24 @@ function _menu_link_translate(&$item) {
 }
 
 /**
+ * Replace wildcards in a path with the dynamic values from a map.
+ *
+ * @param $path
+ *   A path containing wildcards to be replaced.
+ * @param $map
+ *   Array containing string and integer values to be used in the replacement.
+ */
+function _menu_link_path_map_replace($path, $map) {
+  $path_parts = explode('/', $path);
+  foreach ($path_parts as $index => $part) {
+    if ($part == '%' && isset($map[$index])) {
+      $path_parts[$index] = $map[$index];
+    }
+  }
+  return implode('/', $path_parts);
+}
+
+/**
  * Get a loaded object from a router item.
  *
  * menu_get_object() provides access to objects loaded by the current router
@@ -864,7 +884,9 @@ function menu_tree_all_data($menu_name, 
     }
     // If the tree data was not in the cache, $data will be NULL.
     if (!isset($data)) {
-      // Build and run the query, and build the tree.
+      // Select the links from the table, and recursively build the tree. We
+      // LEFT JOIN since there is no match in {menu_router} for an external
+      // link.
       $query = db_select('menu_links', 'ml');
       $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
       $query->fields('ml');
@@ -902,10 +924,10 @@ function menu_tree_all_data($menu_name, 
         // Get all links in this menu.
         $parents = array();
       }
-      // Select the links from the table, and recursively build the tree. We
-      // LEFT JOIN since there is no match in {menu_router} for an external
-      // link.
-      $data['tree'] = menu_tree_data($query->execute(), $parents);
+
+      // Run the query and build the tree.
+      $items = $query->execute()->fetchAllAssoc('mlid', PDO::FETCH_ASSOC);
+      $data['tree'] = menu_tree_data($items, 0, $parents);
       $data['node_links'] = array();
       menu_tree_collect_node_links($data['tree'], $data['node_links']);
       // Cache the data, if it is not already in the cache.
@@ -963,9 +985,9 @@ function menu_tree_page_data($menu_name)
         // Build and run the query, and build the tree.
         if ($item['access']) {
           // Check whether a menu link exists that corresponds to the current path.
-          $args[] = $item['href'];
+          $link_paths = array($item['path'], $item['href']);
           if (drupal_is_front_page()) {
-            $args[] = '<front>';
+            $link_paths[] = '<front>';
           }
           $parents = db_select('menu_links')
             ->fields('menu_links', array(
@@ -979,12 +1001,16 @@ function menu_tree_page_data($menu_name)
               'p8',
             ))
             ->condition('menu_name', $menu_name)
-            ->condition('link_path', $args, 'IN')
+            ->condition('link_path', $link_paths, 'IN')
+            ->orderBy('link_path', 'DESC')
             ->execute()->fetchAssoc();
 
-          if (empty($parents)) {
+          if (empty($parents) && ($item['type'] & MENU_IS_LOCAL_TASK)) {
             // If no link exists, we may be on a local task that's not in the links.
-            // TODO: Handle the case like a local task on a specific node in the menu.
+            $link_paths = array(
+              $item['tab_root'],
+              _menu_link_path_map_replace($item['tab_root'], $item['original_map']),
+            );
             $parents = db_select('menu_links')
               ->fields('menu_links', array(
                 'p1',
@@ -997,7 +1023,8 @@ function menu_tree_page_data($menu_name)
                 'p8',
               ))
               ->condition('menu_name', $menu_name)
-              ->condition('link_path', $item['tab_root'])
+              ->condition('link_path', $link_paths, 'IN')
+              ->orderBy('link_path', 'DESC')
               ->execute()->fetchAssoc();
           }
           // We always want all the top-level links with plid == 0.
@@ -1056,7 +1083,9 @@ function menu_tree_page_data($menu_name)
         }
         $query->condition('ml.menu_name', $menu_name);
         $query->condition('ml.plid', $args, 'IN');
-        $data['tree'] = menu_tree_data($query->execute(), $parents);
+        $items = $query->execute()->fetchAllAssoc('mlid', PDO::FETCH_ASSOC);
+        $map = isset($item['original_map']) ? $item['original_map'] : array();
+        $data['tree'] = menu_tree_data($items, 0, $parents, $map);
         $data['node_links'] = array();
         menu_tree_collect_node_links($data['tree'], $data['node_links']);
         // Cache the data, if it is not already in the cache.
@@ -1163,82 +1192,50 @@ function _menu_tree_check_access(&$tree)
 /**
  * Build the data representing a menu tree.
  *
- * @param $result
- *   The database result.
+ * @param $links
+ *   An array of all links within the menu.
+ * @param $plid
+ *   (optional) A parent menu link ID that that will be the root of the returned
+ *   tree. Defaults to 0, which is the root of the menu.
  * @param $parents
- *   An array of the plid values that represent the path from the current page
- *   to the root of the menu tree.
- * @param $depth
- *   The depth of the current menu tree.
+ *   (optional) An array of the plid values that represent the active path from
+ *   the current page to the root of the menu tree. These parents are used to
+ *   calculate the active trail used by the breadcrumb.
+ * @param $map
+ *   (optional) An array of arguments that map to the active trail within this
+ *   tree. These values are used to expand the titles and URLs within the active
+ *   trail when one of the parents contains a wildcard placeholder.
  * @return
  *   See menu_tree_page_data for a description of the data structure.
  */
-function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {
-  list(, $tree) = _menu_tree_data($result, $parents, $depth);
+function menu_tree_data($links, $plid = 0, $parents = array(), $map = array()) {
+  $tree = array();
+  _menu_tree_data($tree, $links, $plid, $parents, $map);
   return $tree;
 }
 
 /**
  * Recursive helper function to build the data representing a menu tree.
- *
- * The function is a bit complex because the rendering of an item depends on
- * the next menu item. So we are always rendering the element previously
- * processed not the current one.
  */
-function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
-  $remnant = NULL;
-  $tree = array();
-  foreach ($result as $item) {
+function _menu_tree_data(&$tree, $links, $plid, $parents, $map) {
+  foreach ($links as $item) {
     $item = is_object($item) ? get_object_vars($item) : $item;
     // We need to determine if we're on the path to root so we can later build
     // the correct active trail and breadcrumb.
     $item['in_active_trail'] = in_array($item['mlid'], $parents);
-    // The current item is the first in a new submenu.
-    if ($item['depth'] > $depth) {
-      // _menu_tree returns an item and the menu tree structure.
-      list($item, $below) = _menu_tree_data($result, $parents, $item['depth'], $item);
-      if ($previous_element) {
-        $tree[$previous_element['mlid']] = array(
-          'link' => $previous_element,
-          'below' => $below,
-        );
-      }
-      else {
-        $tree = $below;
-      }
-      // We need to fall back one level.
-      if (!isset($item) || $item['depth'] < $depth) {
-        return array($item, $tree);
-      }
-      // This will be the link to be output in the next iteration.
-      $previous_element = $item;
-    }
-    // We are at the same depth, so we use the previous element.
-    elseif ($item['depth'] == $depth) {
-      if ($previous_element) {
-        // Only the first time.
-        $tree[$previous_element['mlid']] = array(
-          'link' => $previous_element,
-          'below' => FALSE,
-        );
-      }
-      // This will be the link to be output in the next iteration.
-      $previous_element = $item;
+
+    if ($item['in_active_trail']) {
+      $item['active_map'] = $map;
     }
-    // The submenu ended with the previous item, so pass back the current item.
-    else {
-      $remnant = $item;
-      break;
+
+    if ($item['plid'] == $plid) {
+      $tree[$item['mlid']] = array(
+        'link' => $item,
+        'below' => array(),
+      );
+      _menu_tree_data($tree[$item['mlid']]['below'], $links, $item['mlid'], $parents, $map);
     }
   }
-  if ($previous_element) {
-    // We have one more link dangling.
-    $tree[$previous_element['mlid']] = array(
-      'link' => $previous_element,
-      'below' => FALSE,
-    );
-  }
-  return array($remnant, $tree);
 }
 
 /**
@@ -1668,16 +1665,9 @@ function menu_set_active_trail($new_trai
       // Thus, replace it with the item corresponding to the root path to get
       // the relevant href and title. For example, the menu item corresponding
       // to 'admin' is used when on the 'By module' tab at 'admin/by-module'.
-      $parts = explode('/', $item['tab_root']);
-      $args = arg();
-      // Replace wildcards in the root path using the current path.
-      foreach ($parts as $index => $part) {
-        if ($part == '%') {
-          $parts[$index] = $args[$index];
-        }
-      }
+      $root_path = _menu_link_path_map_replace($item['tab_root'], arg());
       // Retrieve the menu item using the root path after wildcard replacement.
-      $root_item = menu_get_item(implode('/', $parts));
+      $root_item = menu_get_item($root_path);
       if ($root_item && $root_item['access']) {
         $item = $root_item;
       }
@@ -1687,16 +1677,21 @@ function menu_set_active_trail($new_trai
     // Determine if the current page is a link in any of the active menus.
     if ($menu_names) {
       $query = db_select('menu_links', 'ml');
-      $query->fields('ml', array('menu_name'));
-      $query->condition('ml.link_path', $item['href']);
+      $query->fields('ml', array('menu_name', 'link_path'));
+      $query->condition('link_path', array($item['path'], $item['href']), 'IN');
       $query->condition('ml.menu_name', $menu_names, 'IN');
       $result = $query->execute();
-      $found = array();
+
+      // We match on both dynamic paths containing % wildcards and on exact
+      // paths. Exact paths will take precedence over dynamic values.
+      $menu_matches = array('exact' => array(), 'dynamic' => array());
       foreach ($result as $menu) {
-        $found[] = $menu->menu_name;
+        $match_type = (strpos($menu->link_path, '%') === FALSE) ? 'exact' : 'dynamic';
+        $menu_matches[$match_type][] = $menu->menu_name;
       }
+      $menu_matches = empty($menu_matches['exact']) ? $menu_matches['dynamic'] : $menu_matches['exact'];
       // The $menu_names array is ordered, so take the first one that matches.
-      $name = current(array_intersect($menu_names, $found));
+      $name = current(array_intersect($menu_names, $menu_matches));
       if ($name !== FALSE) {
         $tree = menu_tree_page_data($name);
         list($key, $curr) = each($tree);
@@ -1749,9 +1744,12 @@ function menu_get_active_breadcrumb() {
   $item = menu_get_item();
   if ($item && $item['access']) {
     $active_trail = menu_get_active_trail();
-
+    // Do not include the front page twice in the breadcrumb.
+    $front_url = variable_get('site_frontpage', 'node');
     foreach ($active_trail as $parent) {
-      $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
+      if ($parent['href'] != $front_url) {
+        $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']);
+      }
     }
     $end = end($active_trail);
 
@@ -2134,6 +2132,11 @@ function menu_link_save(&$item) {
       $query->condition('menu_name', $item['menu_name']);
     }
 
+    // Ensure that the parent is not a hidden menu entry when reseting items.
+    if (isset($item['reset'])) {
+      $query->condition('hidden', 0, '>=');
+    }
+
     // Find the parent - it must be unique.
     $parent_path = $item['link_path'];
     do {
@@ -2153,7 +2156,7 @@ function menu_link_save(&$item) {
   $menu_name = $item['menu_name'];
   // Menu callbacks need to be in the links table for breadcrumbs, but can't
   // be parents if they are generated directly from a router item.
-  if (empty($parent['mlid']) || $parent['hidden'] < 0) {
+  if (empty($parent['mlid'])) {
     $item['plid'] =  0;
   }
   else {
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.499
diff -u -p -r1.499 book.module
--- modules/book/book.module	5 Jul 2009 18:00:07 -0000	1.499
+++ modules/book/book.module	6 Jul 2009 04:17:07 -0000
@@ -1190,8 +1190,8 @@ function book_menu_subtree_data($item) {
       for ($i = 1; $i <= MENU_MAX_DEPTH; ++$i) {
         $query->orderBy("p$i");
       }
-
-      $data['tree'] = menu_tree_data($query->execute(), array(), $item['depth']);
+      $items = $query->execute()->fetchAllAssoc('mlid', PDO::FETCH_ASSOC);
+      $data['tree'] = menu_tree_data($items, $item['plid']);
       $data['node_links'] = array();
       menu_tree_collect_node_links($data['tree'], $data['node_links']);
       // Compute the real cid for book subtree data.
Index: modules/book/book.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.test,v
retrieving revision 1.11
diff -u -p -r1.11 book.test
--- modules/book/book.test	12 Jun 2009 08:39:36 -0000	1.11
+++ modules/book/book.test	6 Jul 2009 04:17:07 -0000
@@ -22,7 +22,7 @@ class BookTestCase extends DrupalWebTest
   function testBook() {
     // Create users.
     $book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
-    $web_user = $this->drupalCreateUser(array('access printer-friendly version'));
+    $web_user = $this->drupalCreateUser(array('access printer-friendly version', 'administer book outlines'));
 
     // Create new book.
     $this->drupalLogin($book_author);
@@ -138,6 +138,17 @@ class BookTestCase extends DrupalWebTest
     // Compare expected and got breadcrumbs.
     $this->assertIdentical($expected_breadcrumb, $got_breadcrumb, t('The breadcrumb is correctly displayed on the page.'));
 
+    // Check that the breadcrumb is maintained on the Outline tab.
+    $this->drupalGet('node/' . $node->nid . '/outline');
+    $expected_breadcrumb[] = url('node/' . $node->nid);
+    $links = $this->xpath("//div[@class='breadcrumb']/a");
+    $got_breadcrumb = array();
+    foreach ($links as $link) {
+      $got_breadcrumb[] = (string) $link['href'];
+    }
+
+    $this->assertIdentical($expected_breadcrumb, $got_breadcrumb, t('The breadcrumb is correctly displayed on the edit form.'));
+
     // Check printer friendly version.
     $this->drupalGet('book/export/html/' . $node->nid);
     $this->assertText($node->title, t('Printer friendly title found.'));
Index: modules/menu/menu.admin.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/menu/menu.admin.inc,v
retrieving revision 1.52
diff -u -p -r1.52 menu.admin.inc
--- modules/menu/menu.admin.inc	29 Jun 2009 14:24:56 -0000	1.52
+++ modules/menu/menu.admin.inc	6 Jul 2009 04:17:08 -0000
@@ -47,8 +47,8 @@ function menu_overview_form(&$form_state
     FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
     WHERE ml.menu_name = :menu
     ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
-  $result = db_query($sql, array(':menu' => $menu['menu_name']), array('fetch' => PDO::FETCH_ASSOC));
-  $tree = menu_tree_data($result);
+  $items = db_query($sql, array(':menu' => $menu['menu_name']))->fetchAllAssoc('mlid', PDO::FETCH_ASSOC);
+  $tree = menu_tree_data($items);
   $node_links = array();
   menu_tree_collect_node_links($tree, $node_links);
   // We indicate that a menu administrator is running the menu access check.
Index: modules/menu/menu.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/menu/menu.module,v
retrieving revision 1.194
diff -u -p -r1.194 menu.module
--- modules/menu/menu.module	5 Jul 2009 18:00:09 -0000	1.194
+++ modules/menu/menu.module	6 Jul 2009 04:17:08 -0000
@@ -259,6 +259,7 @@ function menu_reset_item($item) {
   foreach (array('mlid', 'has_children') as $key) {
     $new_item[$key] = $item[$key];
   }
+  $new_item['reset'] = TRUE;
   menu_link_save($new_item);
   return $new_item;
 }
Index: modules/simpletest/tests/menu.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu.test,v
retrieving revision 1.11
diff -u -p -r1.11 menu.test
--- modules/simpletest/tests/menu.test	30 May 2009 11:17:32 -0000	1.11
+++ modules/simpletest/tests/menu.test	6 Jul 2009 04:17:08 -0000
@@ -95,18 +95,56 @@ class MenuIncTestCase extends DrupalWebT
   }
 
   /**
-   * Tests for menu hiearchy.
+   * Tests for menu hierarchy.
    */
   function testMenuHiearchy() {
     $parent_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent'))->fetchAssoc();
     $child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child'))->fetchAssoc();
+    $dynamic_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/%'))->fetchAssoc();
+    $dynamic_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/%/child'))->fetchAssoc();
     $unattached_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child2/child'))->fetchAssoc();
 
     $this->assertEqual($child_link['plid'], $parent_link['mlid'], t('The parent of a directly attached child is correct.'));
+    $this->assertEqual($dynamic_link['plid'], $parent_link['mlid'], t('The parent of a dynamic path callback is correct.'));
+    $this->assertEqual($dynamic_child_link['plid'], $dynamic_link['mlid'], t('The parent of a menu callback underneath a dynamic path is correct.'));
     $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], t('The parent of a non-directly attached child is correct.'));
   }
 
   /**
+   * Tests for the breadcrumb.
+   */
+  function testMenuBreadcrumb() {
+    // Check that the Home link is omitted if on the homepage.
+    $expected_breadcrumb = array();
+    variable_set('site_frontpage', 'menu-test/hierarchy/parent');
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent');
+
+    // Check that the Home link is visible on any non-front page.
+    variable_del('site_frontpage');
+    $expected_breadcrumb[] = url('<front>');
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent');
+
+    // Check that a basic 2nd level breadcrumb is available.
+    $expected_breadcrumb[] = url('menu-test/hierarchy/parent');
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent/child');
+
+    // Check that a 2nd level breadcrumb is available when parents are skipped.
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent/child2/child');
+
+    // Check that the 2nd level breadcrumb is available under a dynamic path.
+    $dynamic_path = $this->randomName();
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent/' . $dynamic_path);
+
+    // Check that the 3rd level breadcrumb is available under a dynamic path.
+    $expected_breadcrumb[] = url('menu-test/hierarchy/parent/' . $dynamic_path);
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent/' . $dynamic_path . '/child');
+
+    // Check that the 4th level breadcrumb is available under a dynamic path tab.
+    $expected_breadcrumb[] = url('menu-test/hierarchy/parent/' . $dynamic_path . '/child');
+    $this->assertBreadcrumb($expected_breadcrumb, 'menu-test/hierarchy/parent/' . $dynamic_path . '/child/tab2');
+  }
+
+  /**
    * Test menu_set_item().
    */
   function testMenuSetItem() {
@@ -123,6 +161,24 @@ class MenuIncTestCase extends DrupalWebT
     $this->assertEqual($compare_item, $item, t('Modified menu item is equal to newly retrieved menu item.'), 'menu');
   }
 
+  /**
+   * Given a path, check that the breadcrumb is correct.
+   */
+  private function assertBreadcrumb($expected_breadcrumb, $path) {
+    $this->drupalGet($path);
+
+    // Fetch links in the current breadcrumb.
+    $links = $this->xpath("//div[@class='breadcrumb']/a");
+    $breadcrumb = array();
+    if (is_array($links)) {
+      foreach ($links as $link) {
+        $breadcrumb[] = (string) $link['href'];
+      }
+    }
+
+    $this->assertIdentical($expected_breadcrumb, $breadcrumb, t('Breadcrumb at @path functioning as expected.', array('@path' => $path)));
+  }
+
 }
 
 /**
Index: modules/simpletest/tests/menu_test.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu_test.info,v
retrieving revision 1.1
diff -u -p -r1.1 menu_test.info
--- modules/simpletest/tests/menu_test.info	28 Dec 2008 18:27:14 -0000	1.1
+++ modules/simpletest/tests/menu_test.info	6 Jul 2009 04:17:08 -0000
@@ -5,4 +5,4 @@ package = Testing
 version = VERSION
 core = 7.x
 files[] = menu_test.module
-hidden = TRUE
+;hidden = TRUE
Index: modules/simpletest/tests/menu_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/menu_test.module,v
retrieving revision 1.5
diff -u -p -r1.5 menu_test.module
--- modules/simpletest/tests/menu_test.module	27 May 2009 18:34:00 -0000	1.5
+++ modules/simpletest/tests/menu_test.module	6 Jul 2009 04:17:08 -0000
@@ -28,31 +28,79 @@ function menu_test_menu() {
   // Hidden link for menu_link_maintain tests
   $items['menu_test_maintain/%'] = array(
     'title' => 'Menu maintain test',
-    'page callback' => 'node_page_default',
+    'page callback' => 'menu_test_callback',
     'access arguments' => array('access content'),
    );
   // Hierarchical tests.
   $items['menu-test/hierarchy/parent'] = array(
     'title' => 'Parent menu router',
-    'page callback' => 'node_page_default',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
   );
   $items['menu-test/hierarchy/parent/child'] = array(
     'title' => 'Child menu router',
-    'page callback' => 'node_page_default',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
+  );
+  $items['menu-test/hierarchy/parent/child/child'] = array(
+    'title' => 'Attached subchild router',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
+  );
+  $items['menu-test/hierarchy/parent/%menu_test'] = array(
+    'title' => 'Dynamic router',
+    'title callback' => 'menu_test_title',
+    'title arguments' => array(3),
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+  $items['menu-test/hierarchy/parent/%menu_test/child'] = array(
+    'title' => 'Dynamic child router',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+  $items['menu-test/hierarchy/parent/%menu_test/child/tab1'] = array(
+    'title' => 'tab 1',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+  );
+  $items['menu-test/hierarchy/parent/%menu_test/child/tab2'] = array(
+    'title' => 'tab 2',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
+    'type' => MENU_LOCAL_TASK,
   );
   $items['menu-test/hierarchy/parent/child2/child'] = array(
     'title' => 'Unattached subchild router',
-    'page callback' => 'node_page_default',
+    'page callback' => 'menu_test_callback',
+    'access callback' => TRUE,
   );
   return $items;
 }
 
 /**
+ * Menu loader.
+ */
+function menu_test_load($text) {
+  return $text . ' loader';
+}
+
+/**
+ * Menu title callback.
+ */
+function menu_test_title($text) {
+  return $text . ' title';
+}
+
+/**
  * Dummy callback for hook_menu() to point to.
  *
  * @return
  *  A random string.
  */
 function menu_test_callback() {
-  return $this->randomName();
+  return 'menu test callback';
 }
