Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1129
diff -u -p -r1.1129 common.inc
--- includes/common.inc	18 Mar 2010 19:15:02 -0000	1.1129
+++ includes/common.inc	21 Mar 2010 21:09:02 -0000
@@ -3315,13 +3315,61 @@ function drupal_html_class($class) {
 /**
  * Prepare a string for use as a valid HTML ID and guarantee uniqueness.
  *
+ * This function ensures that each return value is returned just once per page,
+ * by tracking what it has already returned during the page request. To manage
+ * uniqueness across multiple AJAX requests for the same page, AJAX requests
+ * submit an array of all IDs already used by the page, and this function
+ * adds those to its internal list of tracked IDs.
+ *
+ * By ensuring ID uniqueness, this function enables forms, blocks, and other
+ * parts of a page to be output multiple times on the page without breaking
+ * JavaScript code and XHTML validity, but it accomplishes this by appending a
+ * number after all but the first occurrence and reducing multiple consecutive
+ * hyphens to a single hyphen for even the first occurrence. Therefore,
+ * JavaScript and CSS code should not rely on any explicit value returned by
+ * this function, and instead be made to work with whatever this function
+ * returns.
+ *
  * @param $id
  *   The ID to clean.
  * @return
  *   The cleaned ID.
  */
 function drupal_html_id($id) {
-  $seen_ids = &drupal_static(__FUNCTION__, array());
+  // Prepopulate HTML ids for AJAX requests.
+  // @see ajax.js
+  $seen_ids_init = &drupal_static(__FUNCTION__ . ':init');
+  if (!isset($seen_ids_init)) {
+    // Direct use of $_POST is normally not recommended, as it has not passed
+    // through any FAPI security, but here we are not doing anything risky with
+    // the data.
+    if (empty($_POST['ajax_html_ids'])) {
+      $seen_ids_init = array();
+    }
+    else {
+      // $_POST['ajax_html_ids'] is an array of all HTML IDs used on the page.
+      // We iterate these IDs, parsing them into the base ID and the potential
+      // counter that this function appends.
+      foreach ($_POST['ajax_html_ids'] as $seen_id) {
+        $i = 0;
+        // Here we rely on '--' being used solely for separating a base ID from
+        // a uniqueness counter. We ensure this further down in this function
+        // by removing multi-hyphen occurrences from the passed in $id.
+        $parts = explode('--', $seen_id, 2);
+        if (!empty($parts[1]) && is_numeric($parts[1])) {
+          list($seen_id, $i) = $parts;
+        }
+        if (!isset($seen_ids_init[$seen_id])) {
+          $seen_ids_init[$seen_id] = 1;
+        }
+        if ($i > $seen_ids_init[$seen_id]) {
+          $seen_ids_init[$seen_id] = $i;
+        }
+      }
+    }
+  }
+  $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init);
+
   $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => ''));
 
   // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can
@@ -3332,14 +3380,14 @@ function drupal_html_id($id) {
   // characters as well.
   $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id);
 
-  // Ensure IDs are unique. The first occurrence is held but left alone.
-  // Subsequent occurrences get a number appended to them. This incrementing
-  // will almost certainly break code that relies on explicit HTML IDs in forms
-  // that appear more than once on the page, but the alternative is outputting
-  // duplicate IDs, which would break JS code and XHTML validity anyways. For
-  // now, it's an acceptable stopgap solution.
+  // Ensure IDs are unique by appending a counter after the first occurrence.
+  // The counter needs to be appended with a delimiter that does not exist in
+  // the base ID. Requiring a unique delimiter helps ensure that we really do
+  // return unique IDs and also helps us re-create the $seen_ids array during
+  // AJAX requests.
+  $id = preg_replace('/\-+/', '-', $id);
   if (isset($seen_ids[$id])) {
-    $id = $id . '-' . ++$seen_ids[$id];
+    $id = $id . '--' . ++$seen_ids[$id];
   }
   else {
     $seen_ids[$id] = 1;
Index: misc/ajax.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/ajax.js,v
retrieving revision 1.12
diff -u -p -r1.12 ajax.js
--- misc/ajax.js	10 Mar 2010 15:14:38 -0000	1.12
+++ misc/ajax.js	21 Mar 2010 21:09:03 -0000
@@ -204,6 +204,12 @@ Drupal.ajax.prototype.beforeSubmit = fun
   // find the #ajax binding.
   form_values.push({ name: 'ajax_triggering_element', value: this.formPath });
 
+  // Prevent duplicate HTML ids in the returned markup.
+  // @see drupal_html_id()
+  $('[id]').each(function () {
+    form_values.push({ name: 'ajax_html_ids[]', value: this.id });
+  });
+
   // Insert progressbar or throbber.
   if (this.progress.type == 'bar') {
     var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
Index: modules/field/modules/options/options.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/modules/options/options.test,v
retrieving revision 1.10
diff -u -p -r1.10 options.test
--- modules/field/modules/options/options.test	14 Dec 2009 20:18:55 -0000	1.10
+++ modules/field/modules/options/options.test	21 Mar 2010 21:09:04 -0000
@@ -135,7 +135,7 @@ class OptionsWidgetsTestCase extends Fie
 
     // Display form: with no field data, nothing is checked.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertNoFieldChecked("edit-card-2-$langcode--0");
+    $this->assertNoFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertNoFieldChecked("edit-card-2-$langcode-2");
     $this->assertRaw('Some dangerous &amp; unescaped <strong>markup</strong>', t('Option text was properly filtered.'));
@@ -151,7 +151,7 @@ class OptionsWidgetsTestCase extends Fie
 
     // Display form: check that the right options are selected.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFieldChecked("edit-card-2-$langcode--0");
+    $this->assertFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertFieldChecked("edit-card-2-$langcode-2");
 
@@ -166,7 +166,7 @@ class OptionsWidgetsTestCase extends Fie
 
     // Display form: check that the right options are selected.
     $this->drupalGet('test-entity/' . $entity->ftid .'/edit');
-    $this->assertFieldChecked("edit-card-2-$langcode--0");
+    $this->assertFieldChecked("edit-card-2-$langcode-0");
     $this->assertNoFieldChecked("edit-card-2-$langcode-1");
     $this->assertNoFieldChecked("edit-card-2-$langcode-2");
 
Index: modules/field_ui/field_ui.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field_ui/field_ui.test,v
retrieving revision 1.12
diff -u -p -r1.12 field_ui.test
--- modules/field_ui/field_ui.test	3 Mar 2010 08:01:42 -0000	1.12
+++ modules/field_ui/field_ui.test	21 Mar 2010 21:09:04 -0000
@@ -93,7 +93,7 @@ class FieldUITestCase extends DrupalWebT
     // different entity types; e.g. if a field was added in a node entity, it
     // should also appear in the 'taxonomy term' entity.
     $this->drupalGet('admin/structure/taxonomy/1/fields');
-    $this->assertTrue($this->xpath('//select[@id="edit--add-existing-field-field-name"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.'));
+    $this->assertTrue($this->xpath('//select[@name="_add_existing_field[field_name]"]//option[@value="' . $this->field_name . '"]'), t('Existing field was found in account settings.'));
   }
 
   /**
Index: modules/simpletest/drupal_web_test_case.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v
retrieving revision 1.203
diff -u -p -r1.203 drupal_web_test_case.php
--- modules/simpletest/drupal_web_test_case.php	12 Mar 2010 14:38:37 -0000	1.203
+++ modules/simpletest/drupal_web_test_case.php	21 Mar 2010 21:09:06 -0000
@@ -1642,8 +1642,15 @@ class DrupalWebTestCase extends DrupalTe
               // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
               $post[$key] = urlencode($key) . '=' . urlencode($value);
             }
-            if ($ajax && isset($submit['triggering_element'])) {
-              $post['ajax_triggering_element'] = 'ajax_triggering_element=' . urlencode($submit['triggering_element']);
+            // For AJAX requests, add 'ajax_triggering_element' and
+            // 'ajax_html_ids' to the POST data, as ajax.js does.
+            if ($ajax) {
+              if (isset($submit['triggering_element'])) {
+                $post['ajax_triggering_element'] = 'ajax_triggering_element=' . urlencode($submit['triggering_element']);
+              }
+              foreach ($this->xpath('//*[@id]') as $element) {
+                $post['ajax_html_ids[]'] = $element['id'];
+              }
             }
             $post = implode('&', $post);
           }
Index: modules/simpletest/tests/common.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v
retrieving revision 1.105
diff -u -p -r1.105 common.test
--- modules/simpletest/tests/common.test	3 Mar 2010 08:40:25 -0000	1.105
+++ modules/simpletest/tests/common.test	21 Mar 2010 21:09:06 -0000
@@ -742,15 +742,15 @@ class DrupalHTMLIdentifierTestCase exten
     $this->assertIdentical(drupal_html_id('invalid,./:@\\^`{Üidentifier'), 'invalididentifier', t('Strip invalid characters.'));
 
     // Verify Drupal coding standards are enforced.
-    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name--1', t('Enforce Drupal coding standards.'));
+    $this->assertIdentical(drupal_html_id('ID NAME_[1]'), 'id-name-1', t('Enforce Drupal coding standards.'));
 
     // Reset the static cache so we can ensure the unique id count is at zero.
     drupal_static_reset('drupal_html_id');
 
     // Clean up IDs with invalid starting characters.
     $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id', t('Test the uniqueness of IDs #1.'));
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id-2', t('Test the uniqueness of IDs #2.'));
-    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id-3', t('Test the uniqueness of IDs #3.'));
+    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--2', t('Test the uniqueness of IDs #2.'));
+    $this->assertIdentical(drupal_html_id('test-unique-id'), 'test-unique-id--3', t('Test the uniqueness of IDs #3.'));
   }
 }
 
