Bug 344258. Create nsReferencedElement to track which element is associated with a given ID. r+sr=jst
authorRobert O'Callahan <robert@ocallahan.org>
Wed, 25 Jun 2008 13:55:01 +1200
changeset 15515 8e0a0ecefb6541396606d60d538b162c6d9fb41d
parent 15514 30543128f0562dc48e704bfca40f5c1f05fa21bc
child 15516 51c9ba2e3998367054c1a8dd275f94b6efc9078b
push id278
push userrocallahan@mozilla.com
push dateWed, 25 Jun 2008 02:01:35 +0000
treeherdermozilla-central@51c9ba2e3998 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs344258
milestone1.9.1a1pre
Bug 344258. Create nsReferencedElement to track which element is associated with a given ID. r+sr=jst
content/base/public/Makefile.in
content/base/public/nsIDocument.h
content/base/src/Makefile.in
content/base/src/nsContentUtils.cpp
content/base/src/nsDocument.cpp
content/base/src/nsDocument.h
content/xul/document/src/nsXULDocument.cpp
--- a/content/base/public/Makefile.in
+++ b/content/base/public/Makefile.in
@@ -67,20 +67,21 @@ nsIPrivateDOMImplementation.h \
 nsIContentSerializer.h \
 nsIHTMLToTextSink.h \
 nsIXPathEvaluatorInternal.h \
 mozISanitizingSerializer.h \
 nsCaseTreatment.h \
 nsContentCID.h \
 nsCopySupport.h \
 nsContentCreatorFunctions.h \
+nsDOMFile.h \
 nsLineBreaker.h \
+nsPresShellIterator.h \
+nsReferencedElement.h \
 nsXMLNameSpaceMap.h \
-nsPresShellIterator.h \
-nsDOMFile.h \
 $(NULL)
 
 ifndef DISABLE_XFORMS_HOOKS
 EXPORTS += nsIXFormsUtilityService.h
 endif
 
 SDK_XPIDLSRCS   = \
 		nsISelection.idl  \
--- a/content/base/public/nsIDocument.h
+++ b/content/base/public/nsIDocument.h
@@ -92,18 +92,18 @@ class nsIDocumentObserver;
 class nsBindingManager;
 class nsIDOMNodeList;
 class mozAutoSubtreeModified;
 struct JSObject;
 class nsFrameLoader;
 
 // IID for the nsIDocument interface
 #define NS_IDOCUMENT_IID      \
-{ 0xc81acf0b, 0x2539, 0x47ab, \
-  { 0xa6, 0x04, 0x64, 0x04, 0x07, 0x63, 0xc8, 0x3d } }
+{ 0xc45a4a53, 0x0485, 0x43d5, \
+  { 0x85, 0x95, 0x9f, 0x0b, 0xf4, 0x0d, 0xe9, 0x34 } }
 
 // Flag for AddStyleSheet().
 #define NS_STYLESHEET_FROM_CATALOG                (1 << 0)
 
 //----------------------------------------------------------------------
 
 // Document interface.  This is implemented by all document objects in
 // Gecko.
@@ -256,16 +256,42 @@ public:
   virtual nsresult AddCharSetObserver(nsIObserver* aObserver) = 0;
 
   /**
    * Remove a charset observer.
    */
   virtual void RemoveCharSetObserver(nsIObserver* aObserver) = 0;
 
   /**
+   * This gets fired when the element that an id refers to changes.
+   * This fires at difficult times. It is generally not safe to do anything
+   * which could modify the DOM in any way. Use
+   * nsContentUtils::AddScriptRunner.
+   * @return PR_TRUE to keep the callback in the callback set, PR_FALSE
+   * to remove it.
+   */
+  typedef PRBool (* IDTargetObserver)(nsIContent* aOldContent,
+                                      nsIContent* aNewContent, void* aData);
+
+  /**
+   * Add an IDTargetObserver for a specific ID. The IDTargetObserver
+   * will be fired whenever the content associated with the ID changes
+   * in the future. At most one (aObserver, aData) pair can be registered
+   * for each ID.
+   * @return the content currently associated with the ID.
+   */
+  virtual nsIContent* AddIDTargetObserver(nsIAtom* aID,
+                                          IDTargetObserver aObserver, void* aData) = 0;
+  /**
+   * Remove the (aObserver, aData) pair for a specific ID, if registered.
+   */
+  virtual void RemoveIDTargetObserver(nsIAtom* aID,
+                                      IDTargetObserver aObserver, void* aData) = 0;
+
+  /**
    * Get the Content-Type of this document.
    * (This will always return NS_OK, but has this signature to be compatible
    *  with nsIDOMNSDocument::GetContentType())
    */
   NS_IMETHOD GetContentType(nsAString& aContentType) = 0;
 
   /**
    * Set the Content-Type of this document.
--- a/content/base/src/Makefile.in
+++ b/content/base/src/Makefile.in
@@ -147,16 +147,17 @@ CPPSRCS		= \
 		nsNodeInfo.cpp \
 		nsNodeInfoManager.cpp \
 		nsNodeUtils.cpp \
 		nsObjectLoadingContent.cpp \
 		nsParserUtils.cpp \
 		nsPlainTextSerializer.cpp \
 		nsPropertyTable.cpp \
 		nsRange.cpp \
+		nsReferencedElement.cpp \
 		nsScriptElement.cpp \
 		nsScriptEventManager.cpp \
 		nsScriptLoader.cpp \
 		nsStubDocumentObserver.cpp \
 		nsStubImageDecoderObserver.cpp \
 		nsStubMutationObserver.cpp \
 		nsStyledElement.cpp \
 		nsStyleLinkElement.cpp \
--- a/content/base/src/nsContentUtils.cpp
+++ b/content/base/src/nsContentUtils.cpp
@@ -148,16 +148,17 @@ static NS_DEFINE_CID(kXTFServiceCID, NS_
 #include "nsIPrivateDOMEvent.h"
 #include "nsXULPopupManager.h"
 #include "nsIPermissionManager.h"
 #include "nsIScriptObjectPrincipal.h"
 #include "nsIRunnable.h"
 #include "nsDOMJSUtils.h"
 #include "nsGenericHTMLElement.h"
 #include "nsAttrValue.h"
+#include "nsReferencedElement.h"
 
 #ifdef IBMBIDI
 #include "nsIBidiKeyboard.h"
 #endif
 #include "nsCycleCollectionParticipant.h"
 
 // for ReportToConsole
 #include "nsIStringBundle.h"
@@ -3082,130 +3083,23 @@ nsContentUtils::CheckForBOM(const unsign
     aCharset = "UTF-16LE";
   } else {
     found = PR_FALSE;
   }
 
   return found;
 }
 
-static PRBool EqualExceptRef(nsIURL* aURL1, nsIURL* aURL2)
-{
-  nsCOMPtr<nsIURI> u1;
-  nsCOMPtr<nsIURI> u2;
-
-  nsresult rv = aURL1->Clone(getter_AddRefs(u1));
-  if (NS_SUCCEEDED(rv)) {
-    rv = aURL2->Clone(getter_AddRefs(u2));
-  }
-  if (NS_FAILED(rv))
-    return PR_FALSE;
-
-  nsCOMPtr<nsIURL> url1 = do_QueryInterface(u1);
-  nsCOMPtr<nsIURL> url2 = do_QueryInterface(u2);
-  if (!url1 || !url2) {
-    NS_WARNING("Cloning a URL produced a non-URL");
-    return PR_FALSE;
-  }
-  url1->SetRef(EmptyCString());
-  url2->SetRef(EmptyCString());
-
-  PRBool equal;
-  rv = url1->Equals(url2, &equal);
-  return NS_SUCCEEDED(rv) && equal;
-}
-
 /* static */
 nsIContent*
 nsContentUtils::GetReferencedElement(nsIURI* aURI, nsIContent *aFromContent)
 {
-  nsCOMPtr<nsIURL> url = do_QueryInterface(aURI);
-  if (!url)
-    return nsnull;
-
-  nsCAutoString refPart;
-  url->GetRef(refPart);
-  // Unescape %-escapes in the reference. The result will be in the
-  // origin charset of the URL, hopefully...
-  NS_UnescapeURL(refPart);
-
-  nsCAutoString charset;
-  url->GetOriginCharset(charset);
-  nsAutoString ref;
-  nsresult rv = ConvertStringFromCharset(charset, refPart, ref);
-  if (NS_FAILED(rv)) {
-    CopyUTF8toUTF16(refPart, ref);
-  }
-  if (ref.IsEmpty())
-    return nsnull;
-
-  // Get the current document
-  nsIDocument *doc = aFromContent->GetCurrentDoc();
-  if (!doc)
-    return nsnull;
-
-  // This will be the URI of the document the content belongs to
-  // (the URI of the XBL document if the content is anonymous
-  // XBL content)
-  nsCOMPtr<nsIURL> documentURL = do_QueryInterface(doc->GetDocumentURI());
-  nsIContent* bindingParent = aFromContent->GetBindingParent();
-  PRBool isXBL = PR_FALSE;
-  if (bindingParent) {
-    nsXBLBinding* binding = doc->BindingManager()->GetBinding(bindingParent);
-    if (binding) {
-      // XXX sXBL/XBL2 issue
-      // If this is an anonymous XBL element then the URI is
-      // relative to the binding document. A full fix requires a
-      // proper XBL2 implementation but for now URIs that are
-      // relative to the binding document should be resolve to the
-      // copy of the target element that has been inserted into the
-      // bound document.
-      documentURL = do_QueryInterface(binding->PrototypeBinding()->DocURI());
-      isXBL = PR_TRUE;
-    }
-  }
-  if (!documentURL)
-    return nsnull;
-
-  if (!EqualExceptRef(url, documentURL)) {
-    // Oops -- we don't support off-document references
-    return nsnull;
-  }
-
-  // Get the element
-  nsCOMPtr<nsIContent> content;
-  if (isXBL) {
-    nsCOMPtr<nsIDOMNodeList> anonymousChildren;
-    doc->BindingManager()->
-      GetAnonymousNodesFor(bindingParent, getter_AddRefs(anonymousChildren));
-
-    if (anonymousChildren) {
-      PRUint32 length;
-      anonymousChildren->GetLength(&length);
-      for (PRUint32 i = 0; i < length && !content; ++i) {
-        nsCOMPtr<nsIDOMNode> node;
-        anonymousChildren->Item(i, getter_AddRefs(node));
-        nsCOMPtr<nsIContent> c = do_QueryInterface(node);
-        if (c) {
-          content = MatchElementId(c, ref);
-        }
-      }
-    }
-  } else {
-    nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(doc);
-    NS_ASSERTION(domDoc, "Content doesn't reference a dom Document");
-
-    nsCOMPtr<nsIDOMElement> element;
-    rv = domDoc->GetElementById(ref, getter_AddRefs(element));
-    if (element) {
-      content = do_QueryInterface(element);
-    }
-  }
-
-  return content;
+  nsReferencedElement ref;
+  ref.Reset(aFromContent, aURI);
+  return ref.get();
 }
 
 /* static */
 PRBool
 nsContentUtils::HasNonEmptyAttr(nsIContent* aContent, PRInt32 aNameSpaceID,
                                 nsIAtom* aName)
 {
   static nsIContent::AttrValuesArray strings[] = {&nsGkAtoms::_empty, nsnull};
--- a/content/base/src/nsDocument.cpp
+++ b/content/base/src/nsDocument.cpp
@@ -341,34 +341,91 @@ nsIdentifierMapEntry::GetIdContent(PRBoo
 void
 nsIdentifierMapEntry::AppendAllIdContent(nsCOMArray<nsIContent>* aElements)
 {
   for (PRInt32 i = 0; i < mIdContentList.Count(); ++i) {
     aElements->AppendObject(static_cast<nsIContent*>(mIdContentList[i]));
   }
 }
 
+void
+nsIdentifierMapEntry::AddContentChangeCallback(nsIDocument::IDTargetObserver aCallback,
+                                               void* aData)
+{
+  if (!mChangeCallbacks) {
+    mChangeCallbacks = new nsTHashtable<ChangeCallbackEntry>;
+    if (!mChangeCallbacks)
+      return;
+    mChangeCallbacks->Init();
+  }
+
+  ChangeCallback cc = { aCallback, aData };
+  mChangeCallbacks->PutEntry(cc);
+}
+
+void
+nsIdentifierMapEntry::RemoveContentChangeCallback(nsIDocument::IDTargetObserver aCallback,
+                                                  void* aData)
+{
+  if (!mChangeCallbacks)
+    return;
+  ChangeCallback cc = { aCallback, aData };
+  mChangeCallbacks->RemoveEntry(cc);
+  if (mChangeCallbacks->Count() == 0) {
+    mChangeCallbacks = nsnull;
+  }
+}
+
+struct FireChangeArgs {
+  nsIContent* mFrom;
+  nsIContent* mTo;
+};
+
+PR_STATIC_CALLBACK(PLDHashOperator)
+FireChangeEnumerator(nsIdentifierMapEntry::ChangeCallbackEntry *aEntry, void *aArg)
+{
+  FireChangeArgs* args = static_cast<FireChangeArgs*>(aArg);
+  return aEntry->mKey.mCallback(args->mFrom, args->mTo, aEntry->mKey.mData)
+      ? PL_DHASH_NEXT : PL_DHASH_REMOVE;
+}
+
+void
+nsIdentifierMapEntry::FireChangeCallbacks(nsIContent* aOldContent,
+                                          nsIContent* aNewContent)
+{
+  if (!mChangeCallbacks)
+    return;
+
+  FireChangeArgs args = { aOldContent, aNewContent };
+  mChangeCallbacks->EnumerateEntries(FireChangeEnumerator, &args);
+}
+
 PRBool
 nsIdentifierMapEntry::AddIdContent(nsIContent* aContent)
 {
   NS_PRECONDITION(aContent, "Must have content");
   NS_PRECONDITION(mIdContentList.IndexOf(nsnull) < 0,
                   "Why is null in our list?");
   NS_PRECONDITION(aContent != ID_NOT_IN_DOCUMENT,
                   "Bogus content pointer");
 
-  if (mIdContentList.SafeElementAt(0) == ID_NOT_IN_DOCUMENT) {
+  nsIContent* currentContent = static_cast<nsIContent*>(mIdContentList.SafeElementAt(0));
+  if (currentContent == ID_NOT_IN_DOCUMENT) {
     NS_ASSERTION(mIdContentList.Count() == 1, "Bogus count");
     mIdContentList.ReplaceElementAt(aContent, 0);
+    FireChangeCallbacks(nsnull, aContent);
     return PR_TRUE;
   }
 
   // Common case
   if (mIdContentList.Count() == 0) {
-    return mIdContentList.AppendElement(aContent) != nsnull;
+    if (!mIdContentList.AppendElement(aContent))
+      return PR_FALSE;
+    FireChangeCallbacks(nsnull, aContent);
+    return PR_TRUE;
   }
 
   // We seem to have multiple content nodes for the same id, or we're doing our
   // top-down registration when the id table is going live.  Search for the
   // right place to insert the content.
   PRInt32 start = 0;
   PRInt32 end = mIdContentList.Count();
   do {
@@ -384,27 +441,41 @@ nsIdentifierMapEntry::AddIdContent(nsICo
     }
 
     if (nsContentUtils::PositionIsBefore(aContent, curContent)) {
       end = cur;
     } else {
       start = cur + 1;
     }
   } while (start != end);
-  
-  return mIdContentList.InsertElementAt(aContent, start);
+
+  if (!mIdContentList.InsertElementAt(aContent, start))
+    return PR_FALSE;
+  if (start == 0) {
+    FireChangeCallbacks(currentContent, aContent);
+  }
+  return PR_TRUE;
 }
 
 PRBool
 nsIdentifierMapEntry::RemoveIdContent(nsIContent* aContent)
 {
+  // This should only be called while the document is in an update.
+  // Assertions near the call to this method guarantee this.
+
   // XXXbz should this ever Compact() I guess when all the content is gone
   // we'll just get cleaned up in the natural order of things...
-  return mIdContentList.RemoveElement(aContent) &&
-    mIdContentList.Count() == 0 && !mNameContentList;
+  nsIContent* currentContent = static_cast<nsIContent*>(mIdContentList.SafeElementAt(0));
+  if (!mIdContentList.RemoveElement(aContent))
+    return PR_FALSE;
+  if (currentContent == aContent) {
+    FireChangeCallbacks(currentContent,
+                        static_cast<nsIContent*>(mIdContentList.SafeElementAt(0)));
+  }
+  return mIdContentList.Count() == 0 && !mNameContentList && !mChangeCallbacks;
 }
 
 void
 nsIdentifierMapEntry::FlagIDNotInDocument()
 {
   NS_ASSERTION(mIdContentList.Count() == 0,
                "Flagging ID not in document when we have content?");
   // Note that if this fails that's OK; this is just an optimization
@@ -3094,132 +3165,178 @@ nsDocument::BeginLoad()
   // Block onload here to prevent having to deal with blocking and
   // unblocking it while we know the document is loading.
   BlockOnload();
 
   NS_DOCUMENT_NOTIFY_OBSERVERS(BeginLoad, (this));
 }
 
 PRBool
-nsDocument::CheckGetElementByIdArg(const nsAString& aId)
-{
-  if (aId.IsEmpty()) {
+nsDocument::CheckGetElementByIdArg(const nsIAtom* aId)
+{
+  if (aId == nsGkAtoms::_empty) {
     nsContentUtils::ReportToConsole(
         nsContentUtils::eDOM_PROPERTIES,
         "EmptyGetElementByIdParam",
         nsnull, 0,
         nsnull,
         EmptyString(), 0, 0,
         nsIScriptError::warningFlag,
         "DOM");
     return PR_FALSE;
   }
   return PR_TRUE;
 }
 
+static void
+MatchAllElementsId(nsIContent* aContent, nsIAtom* aId, nsIdentifierMapEntry* aEntry)
+{
+  if (aId == aContent->GetID()) {
+    aEntry->AddIdContent(aContent);
+  }
+
+  PRUint32 i, count = aContent->GetChildCount();
+  for (i = 0; i < count; i++) {
+    MatchAllElementsId(aContent->GetChildAt(i), aId, aEntry);
+  }
+}
+
+nsIdentifierMapEntry*
+nsDocument::GetElementByIdInternal(nsIAtom* aID)
+{
+  // We don't have to flush before we do the initial hashtable lookup, since if
+  // the id is already in the hashtable it couldn't have been removed without
+  // us being notified (all removals notify immediately, as far as I can tell).
+  // So do the lookup first.
+  nsIdentifierMapEntry *entry = mIdentifierMap.PutEntry(aID);
+  NS_ENSURE_TRUE(entry, nsnull);
+
+  if (entry->GetIdContent())
+    return entry;
+
+  // Now we have to flush.  It could be that we have a cached "not in
+  // document" or know nothing about this ID yet but more content has been
+  // added to the document since.  Note that we have to flush notifications,
+  // so that the entry will get updated properly.
+
+  // Make sure to stash away the current generation so we can check whether
+  // the table changes when we flush.
+  PRUint32 generation = mIdentifierMap.GetGeneration();
+  
+  FlushPendingNotifications(Flush_ContentAndNotify);
+
+  if (generation != mIdentifierMap.GetGeneration()) {
+    // Table changed, so the entry pointer is no longer valid; look up the
+    // entry again, adding if necessary (the adding may be necessary in case
+    // the flush actually deleted entries).
+    entry = mIdentifierMap.PutEntry(aID);
+  }
+  
+  PRBool isNotInDocument;
+  nsIContent *e = entry->GetIdContent(&isNotInDocument);
+  if (e || isNotInDocument)
+    return entry;
+
+  // Status of this id is unknown, search document
+  nsIContent* root = GetRootContent();
+  if (!IdTableIsLive()) {
+    if (IdTableShouldBecomeLive()) {
+      // Just make sure our table is up to date and call this method again
+      // to look up in the hashtable.
+      if (root) {
+        RegisterNamedItems(root);
+      }
+      return GetElementByIdInternal(aID);
+    }
+
+    if (root) {
+      // No-one should have registered an ID change callback yet. We don't
+      // want to fire one as a side-effect of getElementById! This shouldn't
+      // happen, since if someone called AddIDTargetObserver already for
+      // this ID, we should have filled in this entry with content or
+      // not-in-document.
+      NS_ASSERTION(!entry->HasContentChangeCallback(),
+                   "No callbacks should be registered while we set up this entry");
+      MatchAllElementsId(root, aID, entry);
+      e = entry->GetIdContent();
+    }
+  }
+
+  if (!e) {
+#ifdef DEBUG
+    // No reason to call MatchElementId if !IdTableIsLive, since
+    // we'd have done just that already
+    if (IdTableIsLive() && root && aID != nsGkAtoms::_empty) {
+      nsIContent* eDebug =
+        nsContentUtils::MatchElementId(root, aID);
+      NS_ASSERTION(!eDebug,
+                   "We got null for |e| but MatchElementId found something?");
+    }
+#endif
+    // There is no element with the given id in the document, cache
+    // the fact that it's not in the document
+    entry->FlagIDNotInDocument();
+    return entry;
+  }
+
+  return entry;
+}
+
 NS_IMETHODIMP
 nsDocument::GetElementById(const nsAString& aElementId,
                            nsIDOMElement** aReturn)
 {
   NS_ENSURE_ARG_POINTER(aReturn);
   *aReturn = nsnull;
 
   nsCOMPtr<nsIAtom> idAtom(do_GetAtom(aElementId));
   NS_ENSURE_TRUE(idAtom, NS_ERROR_OUT_OF_MEMORY);
-
-  // We don't have to flush before we do the initial hashtable lookup, since if
-  // the id is already in the hashtable it couldn't have been removed without
-  // us being notified (all removals notify immediately, as far as I can tell).
-  // So do the lookup first.
-  nsIdentifierMapEntry *entry = mIdentifierMap.PutEntry(idAtom);
+  if (!CheckGetElementByIdArg(idAtom))
+    return NS_OK;
+
+  nsIdentifierMapEntry *entry = GetElementByIdInternal(idAtom);
   NS_ENSURE_TRUE(entry, NS_ERROR_OUT_OF_MEMORY);
 
   PRBool isNotInDocument;
   nsIContent *e = entry->GetIdContent(&isNotInDocument);
-
-  if (!e) {
-    // Now we have to flush.  It could be that we have a cached "not in
-    // document" or know nothing about this ID yet but more content has been
-    // added to the document since.  Note that we have to flush notifications,
-    // so that the entry will get updated properly.
-    
-    // Make sure to stash away the current generation so we can check whether
-    // the table changes when we flush.
-    PRUint32 generation = mIdentifierMap.GetGeneration();
-  
-    FlushPendingNotifications(Flush_ContentAndNotify);
-
-    if (generation != mIdentifierMap.GetGeneration()) {
-      // Table changed, so the entry pointer is no longer valid; look up the
-      // entry again, adding if necessary (the adding may be necessary in case
-      // the flush actually deleted entries).
-      entry = mIdentifierMap.PutEntry(idAtom);
-      NS_ENSURE_TRUE(entry, NS_ERROR_OUT_OF_MEMORY);
-    }
-
-    // We could now have a new entry, or the entry could have been
-    // updated, so update e to point to the current entry's
-    // mIdContent.
-    e = entry->GetIdContent(&isNotInDocument);
-  }
-
-  if (isNotInDocument) {
-    // We've looked for this id before and we didn't find it, so it
-    // won't be in the document now either (since the
-    // mIdentifierMap is live for entries in the table)
-
+  NS_ASSERTION(e || isNotInDocument, "Incomplete map entry!");
+  if (isNotInDocument)
     return NS_OK;
-  }
-
-  if (!e) {
-    // If IdTableIsLive(), no need to look for the element in the document,
-    // since we're fully maintaining our table's state as the DOM mutates.
-    nsIContent* root = GetRootContent();
-    if (!IdTableIsLive()) {
-      if (IdTableShouldBecomeLive()) {
-        // Just make sure our table is up to date and call this method again
-        // to look up in the hashtable.
-        if (root) {
-          RegisterNamedItems(root);
-        }
-        return GetElementById(aElementId, aReturn);
-      }
-
-      if (root && CheckGetElementByIdArg(aElementId)) {
-        e = nsContentUtils::MatchElementId(root, idAtom);
-      }
-    }
-
-    if (!e) {
-#ifdef DEBUG
-      // No reason to call MatchElementId if !IdTableIsLive, since
-      // we'd have done just that already
-      if (IdTableIsLive() && root && !aElementId.IsEmpty()) {
-        nsIContent* eDebug =
-          nsContentUtils::MatchElementId(root, idAtom);
-        NS_ASSERTION(!eDebug,
-                     "We got null for |e| but MatchElementId found something?");
-      }
-#endif
-      // There is no element with the given id in the document, cache
-      // the fact that it's not in the document
-      entry->FlagIDNotInDocument();
-
-      return NS_OK;
-    }
-
-    // We found an element with a matching id, store that in the hash
-    if (NS_UNLIKELY(!entry->AddIdContent(e))) {
-      return NS_ERROR_OUT_OF_MEMORY;
-    }
-  }
 
   return CallQueryInterface(e, aReturn);
 }
 
+nsIContent*
+nsDocument::AddIDTargetObserver(nsIAtom* aID, IDTargetObserver aObserver,
+                                void* aData)
+{
+  if (!CheckGetElementByIdArg(aID))
+    return nsnull;
+
+  nsIdentifierMapEntry *entry = GetElementByIdInternal(aID);
+  NS_ENSURE_TRUE(entry, nsnull);
+
+  entry->AddContentChangeCallback(aObserver, aData);
+  return entry->GetIdContent();
+}
+
+void
+nsDocument::RemoveIDTargetObserver(nsIAtom* aID,
+                                   IDTargetObserver aObserver, void* aData)
+{
+  if (!CheckGetElementByIdArg(aID))
+    return;
+
+  nsIdentifierMapEntry *entry = GetElementByIdInternal(aID);
+  if (!entry)
+    return;
+
+  entry->RemoveContentChangeCallback(aObserver, aData);
+}
+
 void
 nsDocument::DispatchContentLoadedEvents()
 {
   // If you add early returns from this method, make sure you're
   // calling UnblockOnload properly.
   
   // Fire a DOM event notifying listeners that this document has been
   // loaded (excluding images and other loads initiated by this
--- a/content/base/src/nsDocument.h
+++ b/content/base/src/nsDocument.h
@@ -252,35 +252,79 @@ public:
   /**
    * Returns the element if we know the element associated with this
    * id. Otherwise returns null.
    * @param aIsNotInDocument if non-null, we set the output to true
    * if we know for sure the element is not in the document.
    */
   nsIContent* GetIdContent(PRBool* aIsNotInDocument = nsnull);
   void AppendAllIdContent(nsCOMArray<nsIContent>* aElements);
+  /**
+   * This can fire ID change callbacks.
+   * @return true if the content could be added, false if we failed due
+   * to OOM.
+   */
   PRBool AddIdContent(nsIContent* aContent);
   /**
+   * This can fire ID change callbacks.
    * @return true if this map entry should be removed
    */
   PRBool RemoveIdContent(nsIContent* aContent);
   void FlagIDNotInDocument();
 
+  PRBool HasContentChangeCallback() { return mChangeCallbacks != nsnull; }
+  void AddContentChangeCallback(nsIDocument::IDTargetObserver aCallback, void* aData);
+  void RemoveContentChangeCallback(nsIDocument::IDTargetObserver aCallback, void* aData);
+
   void Traverse(nsCycleCollectionTraversalCallback* aCallback);
 
   void SetDocAllList(nsContentList* aContentList) { mDocAllList = aContentList; }
   nsContentList* GetDocAllList() { return mDocAllList; }
 
+  struct ChangeCallback {
+    nsIDocument::IDTargetObserver mCallback;
+    void* mData;
+  };
+
+  struct ChangeCallbackEntry : public PLDHashEntryHdr {
+    typedef const ChangeCallback KeyType;
+    typedef const ChangeCallback* KeyTypePointer;
+
+    ChangeCallbackEntry(const ChangeCallback* key) :
+      mKey(*key) { }
+    ChangeCallbackEntry(const ChangeCallbackEntry& toCopy) :
+      mKey(toCopy.mKey) { }
+
+    KeyType GetKey() const { return mKey; }
+    PRBool KeyEquals(KeyTypePointer aKey) const {
+      return aKey->mCallback == mKey.mCallback &&
+             aKey->mData == mKey.mData;
+    }
+
+    static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+    static PLDHashNumber HashKey(KeyTypePointer aKey)
+    {
+      return NS_PTR_TO_INT32(aKey->mCallback) >> 2 +
+             NS_PTR_TO_INT32(aKey->mData);
+    }
+    enum { ALLOW_MEMMOVE = PR_TRUE };
+    
+    ChangeCallback mKey;
+  };
+
 private:
+  void FireChangeCallbacks(nsIContent* aOldContent, nsIContent* aNewContent);
+
   // The single element ID_NOT_IN_DOCUMENT, or empty to indicate we
   // don't know what element(s) have this key as an ID
   nsSmallVoidArray mIdContentList;
   // NAME_NOT_VALID if this id cannot be used as a 'name'
   nsBaseContentList *mNameContentList;
   nsRefPtr<nsContentList> mDocAllList;
+  nsAutoPtr<nsTHashtable<ChangeCallbackEntry> > mChangeCallbacks;
 };
 
 class nsDocHeaderData
 {
 public:
   nsDocHeaderData(nsIAtom* aField, const nsAString& aData)
     : mField(aField), mData(aData), mNext(nsnull)
   {
@@ -418,16 +462,21 @@ public:
    */
   virtual nsresult AddCharSetObserver(nsIObserver* aObserver);
 
   /**
    * Remove a charset observer.
    */
   virtual void RemoveCharSetObserver(nsIObserver* aObserver);
 
+  virtual nsIContent* AddIDTargetObserver(nsIAtom* aID,
+                                          IDTargetObserver aObserver, void* aData);
+  virtual void RemoveIDTargetObserver(nsIAtom* aID,
+                                      IDTargetObserver aObserver, void* aData);
+
   /**
    * Access HTTP header data (this may also get set from other sources, like
    * HTML META tags).
    */
   virtual void GetHeaderData(nsIAtom* aHeaderField, nsAString& aData) const;
   virtual void SetHeaderData(nsIAtom* aheaderField,
                              const nsAString& aData);
 
@@ -745,17 +794,18 @@ protected:
   void RemoveFromNameTable(nsIContent *aContent);
   void RemoveFromIdTable(nsIContent *aContent);
 
   /**
    * Check that aId is not empty and log a message to the console
    * service if it is.
    * @returns PR_TRUE if aId looks correct, PR_FALSE otherwise.
    */
-  static PRBool CheckGetElementByIdArg(const nsAString& aId);
+  static PRBool CheckGetElementByIdArg(const nsIAtom* aId);
+  nsIdentifierMapEntry* GetElementByIdInternal(nsIAtom* aID);
 
   void DispatchContentLoadedEvents();
 
   void InitializeFinalizeFrameLoaders();
 
   void RetrieveRelevantHeaders(nsIChannel *aChannel);
 
   static PRBool TryChannelCharset(nsIChannel *aChannel,
--- a/content/xul/document/src/nsXULDocument.cpp
+++ b/content/xul/document/src/nsXULDocument.cpp
@@ -1582,22 +1582,23 @@ nsXULDocument::GetCommandDispatcher(nsID
 
 NS_IMETHODIMP
 nsXULDocument::GetElementById(const nsAString& aId,
                               nsIDOMElement** aReturn)
 {
     NS_ENSURE_ARG_POINTER(aReturn);
     *aReturn = nsnull;
 
-    if (!CheckGetElementByIdArg(aId))
-        return NS_OK;
-
     nsCOMPtr<nsIAtom> atom = do_GetAtom(aId);
     if (!atom)
         return NS_ERROR_OUT_OF_MEMORY;
+
+    if (!CheckGetElementByIdArg(atom))
+        return NS_OK;
+
     nsIdentifierMapEntry *entry = mIdentifierMap.GetEntry(atom);
     if (entry) {
         nsIContent* content = entry->GetIdContent();
         if (content)
             return CallQueryInterface(content, aReturn);
     }
     nsRefMapEntry* refEntry = mRefMap.GetEntry(atom);
     if (refEntry) {