Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1222
diff -u -p -r1.1222 common.inc
--- includes/common.inc	17 Sep 2010 14:53:21 -0000	1.1222
+++ includes/common.inc	20 Sep 2010 15:17:35 -0000
@@ -4963,6 +4963,101 @@ function drupal_pre_render_link($element
 }
 
 /**
+ * #pre_render callback that collects nested child links into a single array.
+ *
+ * This function can be added as a pre_render callback for a renderable array,
+ * usually one which will be themed by theme_links(). It iterates recursively
+ * through all unrendered children of the element, collects any #links
+ * properties it finds, merges them into the parent element's #links array, and
+ * prevents those children from being rendered separately.
+ *
+ * The purpose of this is to allow links to be logically grouped into related
+ * categories, so that each child group can be rendered as its own list of
+ * links if drupal_render() is called on it, but calling drupal_render() on the
+ * parent element will still produce a single list containing all the remaining
+ * links, regardless of what group they were in.
+ *
+ * A typical example comes from node links, which are stored in a renderable
+ * array similar to this:
+ * @code
+ * $node->content['links'] = array(
+ *   '#theme' => 'links__node',
+ *   '#pre_render' = array('drupal_pre_render_links'),
+ *   'comment' => array(
+ *     '#theme' => 'links__node__comment',
+ *     '#links' => array(
+ *       // An array of links associated with node comments, suitable for
+ *       // passing in to theme_links().
+ *     ),
+ *   ),
+ *   'statistics' => array(
+ *     '#theme' => 'links__node__statistics',
+ *     '#links' => array(
+ *       // An array of links associated with node statistics, suitable for
+ *       // passing in to theme_links().
+ *     ),
+ *   ),
+ *   'translation' => array(
+ *     '#theme' => 'links__node__translation',
+ *     '#links' => array(
+ *       // An array of links associated with node translation, suitable for
+ *       // passing in to theme_links().
+ *     ),
+ *   ),
+ * );
+ * @endcode
+ *
+ * In this example, the links are grouped by functionality, which can be
+ * helpful to themers who want to display certain kinds of links independently.
+ * For example, adding this code to node.tpl.php will result in the comment
+ * links being rendered as a single list:
+ * @code
+ * print render($content['links']['comment']);
+ * @endcode
+ *
+ * (where $node->content has been transformed into $content before handing
+ * control to the node.tpl.php template).
+ *
+ * The pre_render function defined here allows the above flexibility, but also
+ * allows the following code to be used to render all remaining links into a
+ * single list, regardless of their group:
+ * @code
+ * print render($content['links']);
+ * @endcode
+ *
+ * In the above example, this will result in the statistics and translation
+ * links being rendered together in a single list (but not the comment links,
+ * which were rendered previously on their own).
+ *
+ * Because of the way this function works, the individual properties of each
+ * group (for example, a group-specific #theme property such as
+ * 'links__node__comment' in the example above, or any other property such as
+ * #attributes or #pre_render that is attached to it) are only used when that
+ * group is rendered on its own. When the group is rendered together with other
+ * children, these child-specific properties are ignored, and only the overall
+ * properties of the parent are used.
+ */
+function drupal_pre_render_links($element) {
+  $element += array('#links' => array());
+  foreach (element_children($element) as $key) {
+    $child = &$element[$key];
+    // If the child has not been printed yet and the user has access to it,
+    // merge its links in to the parent.
+    if (empty($child['#printed']) && (!isset($child['#access']) || $child['#access'])) {
+      // The child element itself may have children with #links that need to be
+      // recursively merged. (If not, calling this will at least guarantee that
+      // $child['#links'] is defined, so that it can be used below.)
+      $child = drupal_pre_render_links($child);
+      // Merge in the links and mark the child as having been printed already
+      // (so that its links cannot be mistakenly rendered twice).
+      $element['#links'] += $child['#links'];
+      $child['#printed'] = TRUE;
+    }
+  }
+  return $element;
+}
+
+/**
  * #pre_render callback to append contents in #markup to #children.
  *
  * This needs to be a #pre_render callback, because eventually assigned
Index: modules/blog/blog.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/blog/blog.module,v
retrieving revision 1.360
diff -u -p -r1.360 blog.module
--- modules/blog/blog.module	30 Aug 2010 05:58:46 -0000	1.360
+++ modules/blog/blog.module	20 Sep 2010 15:17:35 -0000
@@ -80,11 +80,16 @@ function blog_view($node, $view_mode) {
 function blog_node_view($node, $view_mode) {
   if ($view_mode != 'rss') {
     if ($node->type == 'blog' && (arg(0) != 'blog' || arg(1) != $node->uid)) {
-      $node->content['links']['#links']['blog_usernames_blog'] = array(
+      $links['blog_usernames_blog'] = array(
         'title' => t("!username's blog", array('!username' => format_username($node))),
         'href' => "blog/$node->uid",
         'attributes' => array('title' => t("Read !username's latest blog entries.", array('!username' => format_username($node)))),
       );
+      $node->content['links']['blog'] = array(
+        '#theme' => 'links__node__blog',
+        '#links' => $links,
+        '#attributes' => array('class' => array('links', 'inline')),
+      );
     }
   }
 }
Index: modules/book/book.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/book/book.module,v
retrieving revision 1.552
diff -u -p -r1.552 book.module
--- modules/book/book.module	9 Sep 2010 23:01:48 -0000	1.552
+++ modules/book/book.module	20 Sep 2010 15:17:35 -0000
@@ -112,7 +112,11 @@ function book_node_view_link($node, $vie
   }
 
   if (!empty($links)) {
-    $node->content['links']['#links'] = array_merge($node->content['links']['#links'], $links);
+    $node->content['links']['book'] = array(
+      '#theme' => 'links__node__book',
+      '#links' => $links,
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
   }
 }
 
Index: modules/comment/comment.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/comment/comment.module,v
retrieving revision 1.898
diff -u -p -r1.898 comment.module
--- modules/comment/comment.module	13 Sep 2010 05:52:18 -0000	1.898
+++ modules/comment/comment.module	20 Sep 2010 15:17:35 -0000
@@ -682,7 +682,11 @@ function comment_node_view($node, $view_
       $links['comment_forbidden']['html'] = TRUE;
     }
 
-    $node->content['links']['#links'] = array_merge($node->content['links']['#links'], $links);
+    $node->content['links']['comment'] = array(
+      '#theme' => 'links__node__comment',
+      '#links' => $links,
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
 
     // Only append comments when we are building a node on its own node detail
     // page. We compare $node and $page_node to ensure that comments are not
@@ -961,9 +965,14 @@ function comment_build_content($comment,
   entity_prepare_view('comment', array($comment->cid => $comment));
   $comment->content += field_attach_view('comment', $comment, $view_mode);
 
+  $comment->content['links'] = array(
+    '#theme' => 'links__comment',
+    '#pre_render' => array('drupal_pre_render_links'),
+    '#attributes' => array('class' => array('links', 'inline')),
+  );
   if (empty($comment->in_preview)) {
     $comment->content['links']['comment'] = array(
-      '#theme' => 'links__comment',
+      '#theme' => 'links__comment__comment',
       '#links' => comment_links($comment, $node),
       '#attributes' => array('class' => array('links', 'inline')),
     );
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1301
diff -u -p -r1.1301 node.module
--- modules/node/node.module	18 Sep 2010 01:39:33 -0000	1.1301
+++ modules/node/node.module	20 Sep 2010 15:17:36 -0000
@@ -1285,6 +1285,11 @@ function node_build_content($node, $view
   // Always display a read more link on teasers because we have no way
   // to know when a teaser view is different than a full view.
   $links = array();
+  $node->content['links'] = array(
+    '#theme' => 'links__node',
+    '#pre_render' => array('drupal_pre_render_links'),
+    '#attributes' => array('class' => array('links', 'inline')),
+  );
   if ($view_mode == 'teaser') {
     $links['node-readmore'] = array(
       'title' => t('Read more'),
@@ -1292,8 +1297,8 @@ function node_build_content($node, $view
       'attributes' => array('rel' => 'tag', 'title' => strip_tags($node->title))
     );
   }
-  $node->content['links'] = array(
-    '#theme' => 'links__node',
+  $node->content['links']['node'] = array(
+    '#theme' => 'links__node__node',
     '#links' => $links,
     '#attributes' => array('class' => array('links', 'inline')),
   );
Index: modules/rdf/rdf.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/rdf/rdf.module,v
retrieving revision 1.45
diff -u -p -r1.45 rdf.module
--- modules/rdf/rdf.module	9 Sep 2010 20:22:00 -0000	1.45
+++ modules/rdf/rdf.module	20 Sep 2010 15:17:36 -0000
@@ -500,7 +500,7 @@ function rdf_preprocess_node(&$variables
   // Adds RDFa markup annotating the number of comments a node has.
   if (isset($variables['node']->comment_count) && !empty($variables['node']->rdf_mapping['comment_count']['predicates'])) {
     // Annotates the 'x comments' link in teaser view.
-    if (isset($variables['content']['links']['#links']['comment-comments'])) {
+    if (isset($variables['content']['links']['comment']['#links']['comment-comments'])) {
       $comment_count_attributes['property'] = $variables['node']->rdf_mapping['comment_count']['predicates'];
       $comment_count_attributes['content'] = $variables['node']->comment_count;
       $comment_count_attributes['datatype'] = $variables['node']->rdf_mapping['comment_count']['datatype'];
@@ -510,7 +510,7 @@ function rdf_preprocess_node(&$variables
       // we set an empty rel attribute which triggers rule number 5. See
       // http://www.w3.org/TR/rdfa-syntax/#sec_5.5.
       $comment_count_attributes['rel'] = '';
-      $variables['content']['links']['#links']['comment-comments']['attributes'] += $comment_count_attributes;
+      $variables['content']['links']['comment']['#links']['comment-comments']['attributes'] += $comment_count_attributes;
     }
     // In full node view, the number of comments is not displayed by
     // node.tpl.php so it is expressed in RDFa in the <head> tag.
Index: modules/simpletest/tests/theme.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/theme.test,v
retrieving revision 1.20
diff -u -p -r1.20 theme.test
--- modules/simpletest/tests/theme.test	22 Aug 2010 12:46:21 -0000	1.20
+++ modules/simpletest/tests/theme.test	20 Sep 2010 15:17:36 -0000
@@ -165,6 +165,132 @@ class ThemeItemListUnitTest extends Drup
 }
 
 /**
+ * Unit tests for theme_links().
+ */
+class ThemeLinksUnitTest extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Themed links',
+      'description' => 'Test the theme_links() function and rendering groups of links.',
+      'group' => 'Theme',
+    );
+  }
+
+  /**
+   * Test the use of drupal_pre_render_links() on a nested array of links.
+   */
+  function testDrupalPreRenderLinks() {
+    // Define the base array to be rendered, containing a variety of different
+    // kinds of links.
+    $base_array = array(
+      '#theme' => 'links',
+      '#pre_render' => array('drupal_pre_render_links'),
+      '#links' => array(
+        'parent_link' => array(
+          'title' => 'Parent link original',
+          'href' => 'parent-link-original',
+        ),
+      ),
+      'first_child' => array(
+        '#theme' => 'links',
+        '#links' => array(
+          // This should be rendered if 'first_child' is rendered separately,
+          // but ignored if the parent is being rendered (since it duplicates
+          // one of the parent's links).
+          'parent_link' => array(
+            'title' => 'Parent link copy',
+            'href' => 'parent-link-copy',
+          ),
+          // This should always be rendered.
+          'first_child_link' => array(
+            'title' => 'First child link',
+            'href' => 'first-child-link',
+          ),
+        ),
+        // This should be ignored if 'first_child' is rendered separately,
+        // since 'first_child' does not have drupal_pre_render_links() as its
+        // own #pre_render function, but it should bubble up to the parent and
+        // be rendered there if the parent itself is being rendered.
+        'first_grandchild' => array(
+          '#theme' => 'links',
+          '#links' => array(
+            'first_grandchild_link' => array(
+              'title' => 'First grandchild link',
+              'href' => 'first-grandchild-link',
+            ),
+          ),
+        ),
+      ),
+      // This should always be rendered as part of the parent.
+      'second_child' => array(
+        '#theme' => 'links',
+        '#links' => array(
+          'second_child_link' => array(
+            'title' => 'Second child link',
+            'href' => 'second-child-link',
+          ),
+        ),
+      ),
+      // This should never be rendered, since the user does not have access to
+      // it.
+      'third_child' => array(
+        '#theme' => 'links',
+        '#links' => array(
+          'third_child_link' => array(
+            'title' => 'Third child link',
+            'href' => 'third-child-link',
+          ),
+        ),
+        '#access' => FALSE,
+      ),
+    );
+
+    // Start with a fresh copy of the base array, and try rendering the entire
+    // thing. We expect a single <ul> with appropriate links contained within
+    // it.
+    $render_array = $base_array;
+    $html = drupal_render($render_array);
+    $dom = new DOMDocument();
+    $dom->loadHTML($html);
+    $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, t('One "ul" tag found in the rendered HTML.'));
+    $list_elements = $dom->getElementsByTagName('li');
+    $this->assertEqual($list_elements->length, 4, t('Four "li" tags found in the rendered HTML.'));
+    $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link original', t('First expected link found.'));
+    $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', t('Second expected link found.'));
+    $this->assertEqual($list_elements->item(2)->nodeValue, 'First grandchild link', t('Third expected link found.'));
+    $this->assertEqual($list_elements->item(3)->nodeValue, 'Second child link', t('Fourth expected link found.'));
+    $this->assertIdentical(strpos($html, 'Parent link copy'), FALSE, t('"Parent link copy" link not found.'));
+    $this->assertIdentical(strpos($html, 'Third child link'), FALSE, t('"Third child link" link not found.'));
+
+    // Now render 'first_child', followed by the rest of the links, and make
+    // sure we get two separate <ul>'s with the appropriate links contained
+    // within each.
+    $render_array = $base_array;
+    $child_html = drupal_render($render_array['first_child']);
+    $parent_html = drupal_render($render_array);
+    // First check the child HTML.
+    $dom = new DOMDocument();
+    $dom->loadHTML($child_html);
+    $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, t('One "ul" tag found in the rendered child HTML.'));
+    $list_elements = $dom->getElementsByTagName('li');
+    $this->assertEqual($list_elements->length, 2, t('Two "li" tags found in the rendered child HTML.'));
+    $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link copy', t('First expected link found.'));
+    $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', t('Second expected link found.'));
+    $this->assertIdentical(strpos($child_html, 'First grandchild link'), FALSE, t('"First grandchild link" link not found.'));
+    // Then check the parent HTML.
+    $dom = new DOMDocument();
+    $dom->loadHTML($parent_html);
+    $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, t('One "ul" tag found in the rendered parent HTML.'));
+    $list_elements = $dom->getElementsByTagName('li');
+    $this->assertEqual($list_elements->length, 2, t('Two "li" tags found in the rendered parent HTML.'));
+    $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link original', t('First expected link found.'));
+    $this->assertEqual($list_elements->item(1)->nodeValue, 'Second child link', t('Second expected link found.'));
+    $this->assertIdentical(strpos($parent_html, 'First child link'), FALSE, t('"First child link" link not found.'));
+    $this->assertIdentical(strpos($parent_html, 'Third child link'), FALSE, t('"Third child link" link not found.'));
+  }
+}
+
+/**
  * Functional test for initialization of the theme system in hook_init().
  */
 class ThemeHookInitUnitTest extends DrupalWebTestCase {
Index: modules/statistics/statistics.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v
retrieving revision 1.338
diff -u -p -r1.338 statistics.module
--- modules/statistics/statistics.module	30 Aug 2010 05:58:46 -0000	1.338
+++ modules/statistics/statistics.module	20 Sep 2010 15:17:36 -0000
@@ -114,12 +114,19 @@ function statistics_permission() {
  */
 function statistics_node_view($node, $view_mode) {
   if ($view_mode != 'rss') {
+    $links = array();
     if (user_access('view post access counter')) {
       $statistics = statistics_get($node->nid);
       if ($statistics) {
-        $node->content['links']['#links']['statistics_counter']['title'] = format_plural($statistics['totalcount'], '1 read', '@count reads');
+        $links['statistics_counter']['title'] = format_plural($statistics['totalcount'], '1 read', '@count reads');
       }
     }
+
+    $node->content['links']['statistics'] = array(
+      '#theme' => 'links__node__statistics',
+      '#links' => $links,
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
   }
 }
 
Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.84
diff -u -p -r1.84 translation.module
--- modules/translation/translation.module	9 Sep 2010 23:01:48 -0000	1.84
+++ modules/translation/translation.module	20 Sep 2010 15:17:36 -0000
@@ -188,7 +188,11 @@ function translation_node_view($node, $v
       $links = $links->links;
       // Do not show link to the same node.
       unset($links[$node->language]);
-      $node->content['links']['#links'] = array_merge($node->content['links']['#links'], $links);
+      $node->content['links']['translation'] = array(
+        '#theme' => 'links__node__translation',
+        '#links' => $links,
+        '#attributes' => array('class' => array('links', 'inline')),
+      );
     }
   }
 }
