Bug 1375701 - Atomize class attribute value in the parser in the innerHTML case. r=Ehsan
☠☠ backed out by 76eecfca4bc6 ☠ ☠
authorHenri Sivonen <hsivonen@hsivonen.fi>
Fri, 11 Aug 2017 09:22:57 +0300
changeset 647027 fabf345eec6e49c8616b30459f1c292ac77e92a6
parent 647026 493584127e470aac52918fbd2b55175605ca4a97
child 647028 76eecfca4bc68248176e48a63efd147e16ec135d
push id74288
push userhikezoe@mozilla.com
push dateWed, 16 Aug 2017 00:19:57 +0000
reviewersEhsan
bugs1375701
milestone57.0a1
Bug 1375701 - Atomize class attribute value in the parser in the innerHTML case. r=Ehsan MozReview-Commit-ID: CKyGlzYS15e
dom/base/Element.cpp
dom/base/Element.h
parser/html/javasrc/MetaScanner.java
parser/html/javasrc/Portability.java
parser/html/javasrc/Tokenizer.java
parser/html/javasrc/TreeBuilder.java
parser/html/nsHtml5MetaScanner.cpp
parser/html/nsHtml5Portability.cpp
parser/html/nsHtml5Portability.h
parser/html/nsHtml5String.cpp
parser/html/nsHtml5String.h
parser/html/nsHtml5Tokenizer.cpp
parser/html/nsHtml5TreeBuilder.cpp
parser/html/nsHtml5TreeOperation.cpp
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -2401,21 +2401,54 @@ Element::OnlyNotifySameValueSet(int32_t 
   }
 
   nsAutoScriptBlocker scriptBlocker;
   nsNodeUtils::AttributeSetToCurrentValue(this, aNamespaceID, aName);
   return true;
 }
 
 nsresult
+Element::SetSingleClassFromParser(nsIAtom* aSingleClassName)
+{
+  // Keep this in sync with SetAttr and SetParsedAttr below.
+
+  if (!mAttrsAndChildren.CanFitMoreAttrs()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsAttrValue value(aSingleClassName);
+
+  nsIDocument* document = GetComposedDoc();
+  mozAutoDocUpdate updateBatch(document, UPDATE_CONTENT_MODEL, false);
+
+  // In principle, BeforeSetAttr should be called here if a node type
+  // existed that wanted to do something special for class, but there
+  // is no such node type, so calling SetMayHaveClass() directly.
+  SetMayHaveClass();
+
+  return SetAttrAndNotify(kNameSpaceID_None,
+                          nsGkAtoms::_class,
+                          nullptr, // prefix
+                          nullptr, // old value
+                          value,
+                          static_cast<uint8_t>(nsIDOMMutationEvent::ADDITION),
+                          false, // hasListeners
+                          false, // notify
+                          kCallAfterSetAttr,
+                          document,
+                          updateBatch);
+}
+
+nsresult
 Element::SetAttr(int32_t aNamespaceID, nsIAtom* aName,
                  nsIAtom* aPrefix, const nsAString& aValue,
                  bool aNotify)
 {
-  // Keep this in sync with SetParsedAttr below
+  // Keep this in sync with SetParsedAttr below and SetSingleClassFromParser
+  // above.
 
   NS_ENSURE_ARG_POINTER(aName);
   NS_ASSERTION(aNamespaceID != kNameSpaceID_Unknown,
                "Don't call SetAttr with unknown namespace");
 
   if (!mAttrsAndChildren.CanFitMoreAttrs()) {
     return NS_ERROR_FAILURE;
   }
@@ -2470,17 +2503,17 @@ Element::SetAttr(int32_t aNamespaceID, n
                           kCallAfterSetAttr, document, updateBatch);
 }
 
 nsresult
 Element::SetParsedAttr(int32_t aNamespaceID, nsIAtom* aName,
                        nsIAtom* aPrefix, nsAttrValue& aParsedValue,
                        bool aNotify)
 {
-  // Keep this in sync with SetAttr above
+  // Keep this in sync with SetAttr and SetSingleClassFromParser above
 
   NS_ENSURE_ARG_POINTER(aName);
   NS_ASSERTION(aNamespaceID != kNameSpaceID_Unknown,
                "Don't call SetAttr with unknown namespace");
 
   if (!mAttrsAndChildren.CanFitMoreAttrs()) {
     return NS_ERROR_FAILURE;
   }
@@ -2704,16 +2737,20 @@ Element::BeforeSetAttr(int32_t aNamespac
 {
   if (aNamespaceID == kNameSpaceID_None) {
     if (aName == nsGkAtoms::_class) {
       if (aValue) {
         // Note: This flag is asymmetrical. It is never unset and isn't exact.
         // If it is ever made to be exact, we probably need to handle this
         // similarly to how ids are handled in PreIdMaybeChange and
         // PostIdMaybeChange.
+        // Note that SetSingleClassFromParser inlines BeforeSetAttr and
+        // calls SetMayHaveClass directly. Making a subclass take action
+        // on the class attribute in a BeforeSetAttr override would
+        // require revising SetSingleClassFromParser.
         SetMayHaveClass();
       }
     }
   }
 
   return NS_OK;
 }
 
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -701,16 +701,23 @@ public:
    */
   bool OnlyNotifySameValueSet(int32_t aNamespaceID, nsIAtom* aName,
                               nsIAtom* aPrefix,
                               const nsAttrValueOrString& aValue,
                               bool aNotify, nsAttrValue& aOldValue,
                               uint8_t* aModType, bool* aHasListeners,
                               bool* aOldValueSet);
 
+  /**
+   * Sets the class attribute to a value that contains no whitespace.
+   * Assumes that we are not notifying and that the attribute hasn't been
+   * set previously.
+   */
+  nsresult SetSingleClassFromParser(nsIAtom* aSingleClassName);
+
   virtual nsresult SetAttr(int32_t aNameSpaceID, nsIAtom* aName, nsIAtom* aPrefix,
                            const nsAString& aValue, bool aNotify) override;
   // aParsedValue receives the old value of the attribute. That's useful if
   // either the input or output value of aParsedValue is StoresOwnData.
   nsresult SetParsedAttr(int32_t aNameSpaceID, nsIAtom* aName, nsIAtom* aPrefix,
                          nsAttrValue& aParsedValue, bool aNotify);
   // GetAttr is not inlined on purpose, to keep down codesize from all
   // the inlined nsAttrValue bits for C++ callers.
--- a/parser/html/javasrc/MetaScanner.java
+++ b/parser/html/javasrc/MetaScanner.java
@@ -792,23 +792,23 @@ public abstract class MetaScanner {
      * @throws SAXException
      */
     private void handleAttributeValue() throws SAXException {
         if (metaState != A) {
             return;
         }
         if (contentIndex == CONTENT.length && content == null) {
             content = Portability.newStringFromBuffer(strBuf, 0, strBufLen
-                 // CPPONLY: , treeBuilder
+                 // CPPONLY: , treeBuilder, false
             );
             return;
         }
         if (charsetIndex == CHARSET.length && charset == null) {
             charset = Portability.newStringFromBuffer(strBuf, 0, strBufLen
-                 // CPPONLY: , treeBuilder
+                 // CPPONLY: , treeBuilder, false
             );
             return;
         }
         if (httpEquivIndex == HTTP_EQUIV.length
                 && httpEquivState == HTTP_EQUIV_NOT_SEEN) {
             httpEquivState = (contentTypeIndex == CONTENT_TYPE.length) ? HTTP_EQUIV_CONTENT_TYPE
                     : HTTP_EQUIV_OTHER;
             return;
--- a/parser/html/javasrc/Portability.java
+++ b/parser/html/javasrc/Portability.java
@@ -35,17 +35,17 @@ public final class Portability {
      * Allocates a new local name object. In C++, the refcount must be set up in such a way that
      * calling <code>releaseLocal</code> on the return value balances the refcount set by this method.
      */
     public static @Local String newLocalNameFromBuffer(@NoLength char[] buf, int offset, int length, Interner interner) {
         return new String(buf, offset, length).intern();
     }
 
     public static String newStringFromBuffer(@NoLength char[] buf, int offset, int length
-        // CPPONLY: , TreeBuilder treeBuilder
+        // CPPONLY: , TreeBuilder treeBuilder, boolean maybeAtomize
     ) {
         return new String(buf, offset, length);
     }
 
     public static String newEmptyString() {
         return "";
     }
 
--- a/parser/html/javasrc/Tokenizer.java
+++ b/parser/html/javasrc/Tokenizer.java
@@ -892,17 +892,17 @@ public class Tokenizer implements Locato
      *
      * <p>
      * C++ memory note: The return value must be released.
      *
      * @return the buffer as a string
      */
     protected String strBufToString() {
         String str = Portability.newStringFromBuffer(strBuf, 0, strBufLen
-            // CPPONLY: , tokenHandler
+            // CPPONLY: , tokenHandler, !newAttributesEachTime && attributeName == AttributeName.CLASS
         );
         clearStrBufAfterUse();
         return str;
     }
 
     /**
      * Returns the buffer as a local name. The return value is released in
      * emitDoctypeToken().
--- a/parser/html/javasrc/TreeBuilder.java
+++ b/parser/html/javasrc/TreeBuilder.java
@@ -3288,17 +3288,17 @@ public abstract class TreeBuilder<T> imp
         }
         String charset = null;
         if (start != -1) {
             if (end == -1) {
                 end = buffer.length;
             }
             charset = Portability.newStringFromBuffer(buffer, start, end
                     - start
-                // CPPONLY: , tb
+                // CPPONLY: , tb, false
             );
         }
         return charset;
     }
 
     private void checkMetaCharset(HtmlAttributes attributes)
             throws SAXException {
         String charset = attributes.getValue(AttributeName.CHARSET);
--- a/parser/html/nsHtml5MetaScanner.cpp
+++ b/parser/html/nsHtml5MetaScanner.cpp
@@ -750,21 +750,23 @@ nsHtml5MetaScanner::addToBuffer(int32_t 
 
 void 
 nsHtml5MetaScanner::handleAttributeValue()
 {
   if (metaState != A) {
     return;
   }
   if (contentIndex == CONTENT.length && !content) {
-    content = nsHtml5Portability::newStringFromBuffer(strBuf, 0, strBufLen, treeBuilder);
+    content = nsHtml5Portability::newStringFromBuffer(
+      strBuf, 0, strBufLen, treeBuilder, false);
     return;
   }
   if (charsetIndex == CHARSET.length && !charset) {
-    charset = nsHtml5Portability::newStringFromBuffer(strBuf, 0, strBufLen, treeBuilder);
+    charset = nsHtml5Portability::newStringFromBuffer(
+      strBuf, 0, strBufLen, treeBuilder, false);
     return;
   }
   if (httpEquivIndex == HTTP_EQUIV.length &&
       httpEquivState == HTTP_EQUIV_NOT_SEEN) {
     httpEquivState = (contentTypeIndex == CONTENT_TYPE.length)
                        ? HTTP_EQUIV_CONTENT_TYPE
                        : HTTP_EQUIV_OTHER;
     return;
--- a/parser/html/nsHtml5Portability.cpp
+++ b/parser/html/nsHtml5Portability.cpp
@@ -11,22 +11,40 @@
 nsIAtom*
 nsHtml5Portability::newLocalNameFromBuffer(char16_t* buf, int32_t offset, int32_t length, nsHtml5AtomTable* interner)
 {
   NS_ASSERTION(!offset, "The offset should always be zero here.");
   NS_ASSERTION(interner, "Didn't get an atom service.");
   return interner->GetAtom(nsDependentSubstring(buf, buf + length));
 }
 
+static bool
+ContainsWhiteSpace(mozilla::Span<char16_t> aSpan)
+{
+  for (char16_t c : aSpan) {
+    if (nsContentUtils::IsHTMLWhitespace(c)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 nsHtml5String
 nsHtml5Portability::newStringFromBuffer(char16_t* buf,
                                         int32_t offset,
                                         int32_t length,
-                                        nsHtml5TreeBuilder* treeBuilder)
+                                        nsHtml5TreeBuilder* treeBuilder,
+                                        bool maybeAtomize)
 {
+  if (!length) {
+    return nsHtml5String::EmptyString();
+  }
+  if (maybeAtomize && !ContainsWhiteSpace(mozilla::MakeSpan(buf + offset, length))) {
+    return nsHtml5String::FromAtom(NS_AtomizeMainThread(nsDependentSubstring(buf + offset, length)));
+  }
   return nsHtml5String::FromBuffer(buf + offset, length, treeBuilder);
 }
 
 nsHtml5String
 nsHtml5Portability::newEmptyString()
 {
   return nsHtml5String::EmptyString();
 }
--- a/parser/html/nsHtml5Portability.h
+++ b/parser/html/nsHtml5Portability.h
@@ -56,17 +56,18 @@ class nsHtml5StateSnapshot;
 
 class nsHtml5Portability
 {
   public:
     static nsIAtom* newLocalNameFromBuffer(char16_t* buf, int32_t offset, int32_t length, nsHtml5AtomTable* interner);
     static nsHtml5String newStringFromBuffer(char16_t* buf,
                                              int32_t offset,
                                              int32_t length,
-                                             nsHtml5TreeBuilder* treeBuilder);
+                                             nsHtml5TreeBuilder* treeBuilder,
+                                             bool maybeAtomize);
     static nsHtml5String newEmptyString();
     static nsHtml5String newStringFromLiteral(const char* literal);
     static nsHtml5String newStringFromString(nsHtml5String string);
     static jArray<char16_t,int32_t> newCharArrayFromLocal(nsIAtom* local);
     static jArray<char16_t, int32_t> newCharArrayFromString(
       nsHtml5String string);
     static nsIAtom* newLocalFromLocal(nsIAtom* local, nsHtml5AtomTable* interner);
     static bool localEqualsBuffer(nsIAtom* local, char16_t* buf, int32_t offset, int32_t length);
--- a/parser/html/nsHtml5String.cpp
+++ b/parser/html/nsHtml5String.cpp
@@ -2,92 +2,59 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsHtml5String.h"
 #include "nsCharTraits.h"
 #include "nsUTF8Utils.h"
 #include "nsHtml5TreeBuilder.h"
 
-nsHtml5String::nsHtml5String(already_AddRefed<nsStringBuffer> aBuffer,
-                             uint32_t aLength)
-  : mBuffer(aBuffer.take())
-  , mLength(aLength)
-{
-  if (mBuffer) {
-    MOZ_ASSERT(aLength);
-  } else {
-    MOZ_ASSERT(!aLength || aLength == UINT32_MAX);
-  }
-}
-
 void
 nsHtml5String::ToString(nsAString& aString)
 {
-  if (mBuffer) {
-    mBuffer->ToString(mLength, aString);
-  } else {
-    aString.Truncate();
-    if (mLength) {
+  switch (GetKind()) {
+    case eStringBuffer:
+      return AsStringBuffer()->ToString(Length(), aString);
+    case eAtom:
+      return AsAtom()->ToString(aString);
+    case eEmpty:
+      aString.Truncate();
+      return;
+    default:
+      aString.Truncate();
       aString.SetIsVoid(true);
-    }
+      return;
   }
 }
 
 void
-nsHtml5String::CopyToBuffer(char16_t* aBuffer)
-{
-  if (mBuffer) {
-    memcpy(aBuffer, mBuffer->Data(), mLength * sizeof(char16_t));
-  }
-}
-
-bool
-nsHtml5String::LowerCaseEqualsASCII(const char* aLowerCaseLiteral)
+nsHtml5String::CopyToBuffer(char16_t* aBuffer) const
 {
-  if (!mBuffer) {
-    if (mLength) {
-      // This string is null
-      return false;
-    }
-    // this string is empty
-    return !(*aLowerCaseLiteral);
-  }
-  return !nsCharTraits<char16_t>::compareLowerCaseToASCIINullTerminated(
-    reinterpret_cast<char16_t*>(mBuffer->Data()), Length(), aLowerCaseLiteral);
+  memcpy(aBuffer, AsPtr(), Length() * sizeof(char16_t));
 }
 
 bool
-nsHtml5String::EqualsASCII(const char* aLiteral)
+nsHtml5String::LowerCaseEqualsASCII(const char* aLowerCaseLiteral) const
 {
-  if (!mBuffer) {
-    if (mLength) {
-      // This string is null
-      return false;
-    }
-    // this string is empty
-    return !(*aLiteral);
-  }
-  return !nsCharTraits<char16_t>::compareASCIINullTerminated(
-    reinterpret_cast<char16_t*>(mBuffer->Data()), Length(), aLiteral);
+  return !nsCharTraits<char16_t>::compareLowerCaseToASCIINullTerminated(
+    AsPtr(), Length(), aLowerCaseLiteral);
 }
 
 bool
-nsHtml5String::LowerCaseStartsWithASCII(const char* aLowerCaseLiteral)
+nsHtml5String::EqualsASCII(const char* aLiteral) const
 {
-  if (!mBuffer) {
-    if (mLength) {
-      // This string is null
-      return false;
-    }
-    // this string is empty
-    return !(*aLowerCaseLiteral);
-  }
+  return !nsCharTraits<char16_t>::compareASCIINullTerminated(
+    AsPtr(), Length(), aLiteral);
+}
+
+bool
+nsHtml5String::LowerCaseStartsWithASCII(const char* aLowerCaseLiteral) const
+{
   const char* litPtr = aLowerCaseLiteral;
-  const char16_t* strPtr = reinterpret_cast<char16_t*>(mBuffer->Data());
+  const char16_t* strPtr = AsPtr();
   const char16_t* end = strPtr + Length();
   char16_t litChar;
   while ((litChar = *litPtr) && (strPtr != end)) {
     MOZ_ASSERT(!(litChar >= 'A' && litChar <= 'Z'),
                "Literal isn't in lower case.");
     char16_t strChar = *strPtr;
     if (strChar >= 'A' && strChar <= 'Z') {
       strChar += 0x20;
@@ -97,57 +64,67 @@ nsHtml5String::LowerCaseStartsWithASCII(
     }
     ++litPtr;
     ++strPtr;
   }
   return true;
 }
 
 bool
-nsHtml5String::Equals(nsHtml5String aOther)
+nsHtml5String::Equals(nsHtml5String aOther) const
 {
   MOZ_ASSERT(operator bool());
   MOZ_ASSERT(aOther);
-  if (mLength != aOther.mLength) {
+  if (Length() != aOther.Length()) {
     return false;
   }
-  if (!mBuffer) {
-    return true;
-  }
-  MOZ_ASSERT(aOther.mBuffer);
   return !memcmp(
-    mBuffer->Data(), aOther.mBuffer->Data(), Length() * sizeof(char16_t));
+    AsPtr(), aOther.AsPtr(), Length() * sizeof(char16_t));
 }
 
 nsHtml5String
 nsHtml5String::Clone()
 {
-  MOZ_ASSERT(operator bool());
-  RefPtr<nsStringBuffer> ref(mBuffer);
-  return nsHtml5String(ref.forget(), mLength);
+  switch (GetKind()) {
+    case eStringBuffer:
+      AsStringBuffer()->AddRef();
+      break;
+    case eAtom:
+      AsAtom()->AddRef();
+      break;
+    default:
+      break;
+  }
+  return nsHtml5String(mBits);
 }
 
 void
 nsHtml5String::Release()
 {
-  if (mBuffer) {
-    mBuffer->Release();
-    mBuffer = nullptr;
+  switch (GetKind()) {
+    case eStringBuffer:
+      AsStringBuffer()->Release();
+      break;
+    case eAtom:
+      AsAtom()->Release();
+      break;
+    default:
+      break;
   }
-  mLength = UINT32_MAX;
+  mBits = eNull;
 }
 
 // static
 nsHtml5String
 nsHtml5String::FromBuffer(char16_t* aBuffer,
                           int32_t aLength,
                           nsHtml5TreeBuilder* aTreeBuilder)
 {
   if (!aLength) {
-    return nsHtml5String(nullptr, 0U);
+    return nsHtml5String(eEmpty);
   }
   // Work with nsStringBuffer directly to make sure that storage is actually
   // nsStringBuffer and to make sure the allocation strategy matches
   // nsAttrValue::GetStringBuffer, so that it doesn't need to reallocate and
   // copy.
   RefPtr<nsStringBuffer> buffer(
     nsStringBuffer::Alloc((aLength + 1) * sizeof(char16_t)));
   if (!buffer) {
@@ -158,68 +135,75 @@ nsHtml5String::FromBuffer(char16_t* aBuf
     buffer = nsStringBuffer::Alloc(2 * sizeof(char16_t));
     if (!buffer) {
       MOZ_CRASH(
         "Out of memory so badly that couldn't even allocate placeholder.");
     }
     char16_t* data = reinterpret_cast<char16_t*>(buffer->Data());
     data[0] = 0xFFFD;
     data[1] = 0;
-    return nsHtml5String(buffer.forget(), 1);
+    return nsHtml5String(reinterpret_cast<uintptr_t>(buffer.forget().take()) | eStringBuffer);
   }
   char16_t* data = reinterpret_cast<char16_t*>(buffer->Data());
   memcpy(data, aBuffer, aLength * sizeof(char16_t));
   data[aLength] = 0;
-  return nsHtml5String(buffer.forget(), aLength);
+  return nsHtml5String(reinterpret_cast<uintptr_t>(buffer.forget().take()) | eStringBuffer);
 }
 
 // static
 nsHtml5String
 nsHtml5String::FromLiteral(const char* aLiteral)
 {
   size_t length = std::strlen(aLiteral);
   if (!length) {
-    return nsHtml5String(nullptr, 0U);
+    return nsHtml5String(eEmpty);
   }
   // Work with nsStringBuffer directly to make sure that storage is actually
   // nsStringBuffer and to make sure the allocation strategy matches
   // nsAttrValue::GetStringBuffer, so that it doesn't need to reallocate and
   // copy.
   RefPtr<nsStringBuffer> buffer(
     nsStringBuffer::Alloc((length + 1) * sizeof(char16_t)));
   if (!buffer) {
     MOZ_CRASH("Out of memory.");
   }
   char16_t* data = reinterpret_cast<char16_t*>(buffer->Data());
   LossyConvertEncoding8to16 converter(data);
   converter.write(aLiteral, length);
   data[length] = 0;
-  return nsHtml5String(buffer.forget(), length);
+  return nsHtml5String(reinterpret_cast<uintptr_t>(buffer.forget().take()) | eStringBuffer);
 }
 
 // static
 nsHtml5String
 nsHtml5String::FromString(const nsAString& aString)
 {
   auto length = aString.Length();
   if (!length) {
-    return nsHtml5String(nullptr, 0U);
+    return nsHtml5String(eEmpty);
   }
   RefPtr<nsStringBuffer> buffer = nsStringBuffer::FromString(aString);
-  if (buffer) {
-    return nsHtml5String(buffer.forget(), length);
+  if (buffer && (length == buffer->StorageSize()/sizeof(char16_t) - 1)) {
+    return nsHtml5String(reinterpret_cast<uintptr_t>(buffer.forget().take()) | eStringBuffer);
   }
   buffer = nsStringBuffer::Alloc((length + 1) * sizeof(char16_t));
   if (!buffer) {
     MOZ_CRASH("Out of memory.");
   }
   char16_t* data = reinterpret_cast<char16_t*>(buffer->Data());
   memcpy(data, aString.BeginReading(), length * sizeof(char16_t));
   data[length] = 0;
-  return nsHtml5String(buffer.forget(), length);
+  return nsHtml5String(reinterpret_cast<uintptr_t>(buffer.forget().take()) | eStringBuffer);
+}
+
+// static
+nsHtml5String
+nsHtml5String::FromAtom(already_AddRefed<nsIAtom> aAtom)
+{
+  return nsHtml5String(reinterpret_cast<uintptr_t>(aAtom.take()) | eAtom);
 }
 
 // static
 nsHtml5String
 nsHtml5String::EmptyString()
 {
-  return nsHtml5String(nullptr, 0U);
+  return nsHtml5String(eEmpty);
 }
--- a/parser/html/nsHtml5String.h
+++ b/parser/html/nsHtml5String.h
@@ -1,95 +1,153 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef nsHtml5String_h
 #define nsHtml5String_h
 
 #include "nsString.h"
+#include "nsIAtom.h"
 
 class nsHtml5TreeBuilder;
 
 /**
- * A pass-by-value type that combines an unsafe `nsStringBuffer*` with its
- * logical length (`uint32_t`). (`nsStringBuffer` knows its capacity but not
- * its logical length, i.e. how much of the capacity is in use.)
+ * A pass-by-value type that can represent 
+ *  * nullptr
+ *  * empty string
+ *  * Non-empty string as exactly-sized (capacity is length) `nsStringBuffer*`
+ *  * Non-empty string as an nsIAtom*
  *
  * Holding or passing this type is as unsafe as holding or passing
- * `nsStringBuffer*`.
- *
- * Empty strings and null strings are distinct. Since an empty nsString does
- * not have a an `nsStringBuffer`, both empty and null `nsHtml5String` have
- * `nullptr` as `mBuffer`. If `mBuffer` is `nullptr`, the empty case is marked
- * with `mLength` being zero and the null case with `mLength` being non-zero.
+ * `nsStringBuffer*`/`nsIAtom*`.
  */
 class nsHtml5String final
 {
+private:
+
+  static const uintptr_t kKindMask = uintptr_t(3);
+
+  static const uintptr_t kPtrMask = ~kKindMask;
+
+  enum Kind : uintptr_t {
+    eNull = 0,
+    eEmpty = 1,
+    eStringBuffer = 2,
+    eAtom = 3,
+  };
+
+  inline Kind GetKind() const { return (Kind)(mBits & kKindMask); }
+
+  inline nsStringBuffer* AsStringBuffer() const
+  {
+    MOZ_ASSERT(GetKind() == eStringBuffer);
+    return reinterpret_cast<nsStringBuffer*>(mBits & kPtrMask);
+  }
+
+  inline nsIAtom* AsAtom() const
+  {
+    MOZ_ASSERT(GetKind() == eAtom);
+    return reinterpret_cast<nsIAtom*>(mBits & kPtrMask);
+  }
+
+  inline const char16_t* AsPtr() const
+  {
+    switch (GetKind()) {
+      case eStringBuffer:
+        return reinterpret_cast<char16_t*>(AsStringBuffer()->Data());
+      case eAtom:
+        return AsAtom()->GetUTF16String();
+      default:
+        return nullptr;
+    }
+  }
+
 public:
   /**
    * Default constructor.
    */
   inline nsHtml5String()
     : nsHtml5String(nullptr)
   {
   }
 
   /**
    * Constructor from nullptr.
    */
   inline MOZ_IMPLICIT nsHtml5String(decltype(nullptr))
-    : mBuffer(nullptr)
-    , mLength(UINT32_MAX)
+    : mBits(eNull)
   {
   }
 
-  inline uint32_t Length() const { return mBuffer ? mLength : 0; }
+  inline uint32_t Length() const
+  {
+    switch (GetKind()) {
+      case eStringBuffer:
+        return (AsStringBuffer()->StorageSize()/sizeof(char16_t) - 1);
+      case eAtom:
+        return AsAtom()->GetLength();
+      default:
+        return 0;
+    }
+  }
 
   /**
    * False iff the string is logically null
    */
-  inline MOZ_IMPLICIT operator bool() const { return !(!mBuffer && mLength); }
+  inline MOZ_IMPLICIT operator bool() const { return mBits; }
+
+  /**
+   * Get the underlying nsIAtom* or nullptr if this nsHtml5String
+   * does not hold an atom.
+   */
+  inline nsIAtom* MaybeAsAtom()
+  {
+    if (GetKind() == eAtom) {
+      return AsAtom();
+    }
+    return nullptr;
+  }
 
   void ToString(nsAString& aString);
 
-  void CopyToBuffer(char16_t* aBuffer);
+  void CopyToBuffer(char16_t* aBuffer) const;
 
-  bool LowerCaseEqualsASCII(const char* aLowerCaseLiteral);
+  bool LowerCaseEqualsASCII(const char* aLowerCaseLiteral) const;
 
-  bool EqualsASCII(const char* aLiteral);
+  bool EqualsASCII(const char* aLiteral) const;
 
-  bool LowerCaseStartsWithASCII(const char* aLowerCaseLiteral);
+  bool LowerCaseStartsWithASCII(const char* aLowerCaseLiteral) const;
 
-  bool Equals(nsHtml5String aOther);
+  bool Equals(nsHtml5String aOther) const;
 
   nsHtml5String Clone();
 
   void Release();
 
   static nsHtml5String FromBuffer(char16_t* aBuffer,
                                   int32_t aLength,
                                   nsHtml5TreeBuilder* aTreeBuilder);
 
   static nsHtml5String FromLiteral(const char* aLiteral);
 
   static nsHtml5String FromString(const nsAString& aString);
 
+  static nsHtml5String FromAtom(already_AddRefed<nsIAtom> aAtom);
+
   static nsHtml5String EmptyString();
 
 private:
-  /**
-   * Constructor from raw parts.
-   */
-  nsHtml5String(already_AddRefed<nsStringBuffer> aBuffer, uint32_t aLength);
 
   /**
-   * nullptr if the string is logically null or logically empty
+   * Constructor from raw bits.
    */
-  nsStringBuffer* mBuffer;
+  nsHtml5String(uintptr_t aBits) : mBits(aBits) {};
 
   /**
-   * The length of the string. non-zero if the string is logically null.
+   * Zero if null, one if empty, otherwise tagged pointer
+   * to either nsIAtom or nsStringBuffer. The two least-significant
+   * bits are tag bits.
    */
-  uint32_t mLength;
+  uintptr_t mBits;
 };
 
 #endif // nsHtml5String_h
--- a/parser/html/nsHtml5Tokenizer.cpp
+++ b/parser/html/nsHtml5Tokenizer.cpp
@@ -226,18 +226,23 @@ nsHtml5Tokenizer::emitOrAppendCharRefBuf
       charRefBufLen = 0;
     }
   }
 }
 
 nsHtml5String
 nsHtml5Tokenizer::strBufToString()
 {
-  nsHtml5String str =
-    nsHtml5Portability::newStringFromBuffer(strBuf, 0, strBufLen, tokenHandler);
+  nsHtml5String str = nsHtml5Portability::newStringFromBuffer(
+    strBuf,
+    0,
+    strBufLen,
+    tokenHandler,
+    !newAttributesEachTime &&
+      attributeName == nsHtml5AttributeName::ATTR_CLASS);
   clearStrBufAfterUse();
   return str;
 }
 
 void 
 nsHtml5Tokenizer::strBufToDoctypeName()
 {
   doctypeName = nsHtml5Portability::newLocalNameFromBuffer(strBuf, 0, strBufLen, interner);
--- a/parser/html/nsHtml5TreeBuilder.cpp
+++ b/parser/html/nsHtml5TreeBuilder.cpp
@@ -2215,18 +2215,18 @@ nsHtml5TreeBuilder::extractCharsetFromCo
     }
   }
   charsetloop_end: ;
     nsHtml5String charset = nullptr;
     if (start != -1) {
       if (end == -1) {
         end = buffer.length;
       }
-      charset =
-        nsHtml5Portability::newStringFromBuffer(buffer, start, end - start, tb);
+      charset = nsHtml5Portability::newStringFromBuffer(
+        buffer, start, end - start, tb, false);
   }
   return charset;
 }
 
 void 
 nsHtml5TreeBuilder::checkMetaCharset(nsHtml5HtmlAttributes* attributes)
 {
   nsHtml5String charset =
--- a/parser/html/nsHtml5TreeOperation.cpp
+++ b/parser/html/nsHtml5TreeOperation.cpp
@@ -408,45 +408,51 @@ nsHtml5TreeOperation::CreateHTMLElement(
   }
 
   if (!aAttributes) {
     return newContent;
   }
 
   int32_t len = aAttributes->getLength();
   for (int32_t i = 0; i < len; i++) {
-    // prefix doesn't need regetting. it is always null or a static atom
-    // local name is never null
-    nsCOMPtr<nsIAtom> localName =
-      Reget(aAttributes->getLocalNameNoBoundsCheck(i));
-    nsCOMPtr<nsIAtom> prefix = aAttributes->getPrefixNoBoundsCheck(i);
-    int32_t nsuri = aAttributes->getURINoBoundsCheck(i);
+    nsHtml5String val = aAttributes->getValueNoBoundsCheck(i);
+    nsIAtom* klass = val.MaybeAsAtom();
+    if (klass) {
+      newContent->SetSingleClassFromParser(klass);
+    } else {
+      // prefix doesn't need regetting. it is always null or a static atom
+      // local name is never null
+      nsCOMPtr<nsIAtom> localName =
+        Reget(aAttributes->getLocalNameNoBoundsCheck(i));
+      nsCOMPtr<nsIAtom> prefix = aAttributes->getPrefixNoBoundsCheck(i);
+      int32_t nsuri = aAttributes->getURINoBoundsCheck(i);
 
-    nsString value; // Not Auto, because using it to hold nsStringBuffer*
-    aAttributes->getValueNoBoundsCheck(i).ToString(value);
-    if (nsGkAtoms::a == aName && nsGkAtoms::name == localName) {
-      // This is an HTML5-incompliant Geckoism.
-      // Remove when fixing bug 582361
-      NS_ConvertUTF16toUTF8 cname(value);
-      NS_ConvertUTF8toUTF16 uv(nsUnescape(cname.BeginWriting()));
-      newContent->SetAttr(nsuri,
-                          localName,
-                          prefix,
-                          uv,
-                          false);
-    } else {
-      newContent->SetAttr(nsuri,
-                          localName,
-                          prefix,
-                          value,
-                          false);
+      nsString value; // Not Auto, because using it to hold nsStringBuffer*
+      val.ToString(value);
+      if (nsGkAtoms::a == aName && nsGkAtoms::name == localName) {
+        // This is an HTML5-incompliant Geckoism.
+        // Remove when fixing bug 582361
+        NS_ConvertUTF16toUTF8 cname(value);
+        NS_ConvertUTF8toUTF16 uv(nsUnescape(cname.BeginWriting()));
+        newContent->SetAttr(nsuri,
+                            localName,
+                            prefix,
+                            uv,
+                            false);
+      } else {
+        newContent->SetAttr(nsuri,
+                            localName,
+                            prefix,
+                            value,
+                            false);
 
-      // Custom element setup may be needed if there is an "is" attribute.
-      if (kNameSpaceID_None == nsuri && !prefix && nsGkAtoms::is == localName) {
-        nsContentUtils::SetupCustomElement(newContent, &value);
+        // Custom element setup may be needed if there is an "is" attribute.
+        if (kNameSpaceID_None == nsuri && !prefix && nsGkAtoms::is == localName) {
+          nsContentUtils::SetupCustomElement(newContent, &value);
+        }
       }
     }
   }
   return newContent;
 }
 
 nsIContent*
 nsHtml5TreeOperation::CreateSVGElement(
@@ -492,26 +498,32 @@ nsHtml5TreeOperation::CreateSVGElement(
   }
 
   if (!aAttributes) {
     return newContent;
   }
 
   int32_t len = aAttributes->getLength();
   for (int32_t i = 0; i < len; i++) {
-    // prefix doesn't need regetting. it is always null or a static atom
-    // local name is never null
-    nsCOMPtr<nsIAtom> localName =
-      Reget(aAttributes->getLocalNameNoBoundsCheck(i));
-    nsCOMPtr<nsIAtom> prefix = aAttributes->getPrefixNoBoundsCheck(i);
-    int32_t nsuri = aAttributes->getURINoBoundsCheck(i);
+    nsHtml5String val = aAttributes->getValueNoBoundsCheck(i);
+    nsIAtom* klass = val.MaybeAsAtom();
+    if (klass) {
+      newContent->SetSingleClassFromParser(klass);
+    } else {
+      // prefix doesn't need regetting. it is always null or a static atom
+      // local name is never null
+      nsCOMPtr<nsIAtom> localName =
+        Reget(aAttributes->getLocalNameNoBoundsCheck(i));
+      nsCOMPtr<nsIAtom> prefix = aAttributes->getPrefixNoBoundsCheck(i);
+      int32_t nsuri = aAttributes->getURINoBoundsCheck(i);
 
-    nsString value; // Not Auto, because using it to hold nsStringBuffer*
-    aAttributes->getValueNoBoundsCheck(i).ToString(value);
-    newContent->SetAttr(nsuri, localName, prefix, value, false);
+      nsString value; // Not Auto, because using it to hold nsStringBuffer*
+      val.ToString(value);
+      newContent->SetAttr(nsuri, localName, prefix, value, false);
+    }
   }
   return newContent;
 }
 
 nsIContent*
 nsHtml5TreeOperation::CreateMathMLElement(nsIAtom* aName,
                                           nsHtml5HtmlAttributes* aAttributes,
                                           nsNodeInfoManager* aNodeInfoManager,
@@ -540,26 +552,32 @@ nsHtml5TreeOperation::CreateMathMLElemen
   aBuilder->HoldElement(newElement.forget());
 
   if (!aAttributes) {
     return newContent;
   }
 
   int32_t len = aAttributes->getLength();
   for (int32_t i = 0; i < len; i++) {
-    // prefix doesn't need regetting. it is always null or a static atom
-    // local name is never null
-    nsCOMPtr<nsIAtom> localName =
-      Reget(aAttributes->getLocalNameNoBoundsCheck(i));
-    nsCOMPtr<nsIAtom> prefix = aAttributes->getPrefixNoBoundsCheck(i);
-    int32_t nsuri = aAttributes->getURINoBoundsCheck(i);
+    nsHtml5String val = aAttributes->getValueNoBoundsCheck(i);
+    nsIAtom* klass = val.MaybeAsAtom();
+    if (klass) {
+      newContent->SetSingleClassFromParser(klass);
+    } else {
+      // prefix doesn't need regetting. it is always null or a static atom
+      // local name is never null
+      nsCOMPtr<nsIAtom> localName =
+        Reget(aAttributes->getLocalNameNoBoundsCheck(i));
+      nsCOMPtr<nsIAtom> prefix = aAttributes->getPrefixNoBoundsCheck(i);
+      int32_t nsuri = aAttributes->getURINoBoundsCheck(i);
 
-    nsString value; // Not Auto, because using it to hold nsStringBuffer*
-    aAttributes->getValueNoBoundsCheck(i).ToString(value);
-    newContent->SetAttr(nsuri, localName, prefix, value, false);
+      nsString value; // Not Auto, because using it to hold nsStringBuffer*
+      val.ToString(value);
+      newContent->SetAttr(nsuri, localName, prefix, value, false);
+    }
   }
   return newContent;
 }
 
 void
 nsHtml5TreeOperation::SetFormElement(nsIContent* aNode, nsIContent* aParent)
 {
   nsCOMPtr<nsIFormControl> formControl(do_QueryInterface(aNode));