/* 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/. */

#include "Sanitizer.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Span.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/BindingUtils.h"
#include "mozilla/dom/CustomElementRegistry.h"
#include "mozilla/dom/DocumentFragment.h"
#include "mozilla/dom/HTMLTemplateElement.h"
#include "mozilla/dom/SanitizerBinding.h"
#include "mozilla/dom/SanitizerDefaultConfig.h"
#include "nsContentUtils.h"
#include "nsFmtString.h"
#include "nsGenericHTMLElement.h"
#include "nsIContentInlines.h"
#include "nsNameSpaceManager.h"

namespace mozilla::dom {
using namespace sanitizer;

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Sanitizer, mGlobal)

NS_IMPL_CYCLE_COLLECTING_ADDREF(Sanitizer)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Sanitizer)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Sanitizer)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

// Map[ElementName -> ?Set[Attributes]]
using ElementsWithAttributes =
    nsTHashMap<const nsStaticAtom*, UniquePtr<StaticAtomSet>>;

StaticAutoPtr<ElementsWithAttributes> sDefaultHTMLElements;
StaticAutoPtr<ElementsWithAttributes> sDefaultMathMLElements;
StaticAutoPtr<ElementsWithAttributes> sDefaultSVGElements;
StaticAutoPtr<StaticAtomSet> sDefaultAttributes;

JSObject* Sanitizer::WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) {
  return Sanitizer_Binding::Wrap(aCx, this, aGivenProto);
}

/* static */
// https://wicg.github.io/sanitizer-api/#sanitizerconfig-get-a-sanitizer-instance-from-options
already_AddRefed<Sanitizer> Sanitizer::GetInstance(
    nsIGlobalObject* aGlobal,
    const OwningSanitizerOrSanitizerConfigOrSanitizerPresets& aOptions,
    bool aSafe, ErrorResult& aRv) {
  // Step 4. If sanitizerSpec is a string:
  if (aOptions.IsSanitizerPresets()) {
    // Step 4.1. Assert: sanitizerSpec is "default"
    MOZ_ASSERT(aOptions.GetAsSanitizerPresets() == SanitizerPresets::Default);

    // Step 4.2. Set sanitizerSpec to the built-in safe default configuration.
    // NOTE: The built-in safe default configuration is complete and not
    // influenced by |safe|.
    RefPtr<Sanitizer> sanitizer = new Sanitizer(aGlobal);
    sanitizer->SetDefaultConfig();
    return sanitizer.forget();
  }

  // Step 5. Assert: sanitizerSpec is either a Sanitizer instance, or a
  // dictionary. Step 6. If sanitizerSpec is a dictionary:
  if (aOptions.IsSanitizerConfig()) {
    // Step 6.1. Let sanitizer be a new Sanitizer instance.
    RefPtr<Sanitizer> sanitizer = new Sanitizer(aGlobal);

    // Step 6.2. Let setConfigurationResult be the result of set a
    // configuration with sanitizerSpec and not safe on sanitizer.
    sanitizer->SetConfig(aOptions.GetAsSanitizerConfig(), !aSafe, aRv);

    // Step 6.3. If setConfigurationResult is false, throw a TypeError.
    if (aRv.Failed()) {
      return nullptr;
    }

    // Step 6.4. Set sanitizerSpec to sanitizer.
    return sanitizer.forget();
  }

  // Step 7. Assert: sanitizerSpec is a Sanitizer instance.
  MOZ_ASSERT(aOptions.IsSanitizer());

  // Step 8. Return sanitizerSpec.
  RefPtr<Sanitizer> sanitizer = aOptions.GetAsSanitizer();
  return sanitizer.forget();
}

/* static */
// https://wicg.github.io/sanitizer-api/#sanitizer-constructor
already_AddRefed<Sanitizer> Sanitizer::Constructor(
    const GlobalObject& aGlobal,
    const SanitizerConfigOrSanitizerPresets& aConfig, ErrorResult& aRv) {
  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
  RefPtr<Sanitizer> sanitizer = new Sanitizer(global);

  // Step 1. If configuration is a SanitizerPresets string, then:
  if (aConfig.IsSanitizerPresets()) {
    // Step 1.1. Assert: configuration is default.
    MOZ_ASSERT(aConfig.GetAsSanitizerPresets() == SanitizerPresets::Default);

    // Step 1.2. Set configuration to the built-in safe default configuration.
    sanitizer->SetDefaultConfig();

    // NOTE: Early return because we don't need to do any
    // processing/verification of the default config.
    return sanitizer.forget();
  }

  // Step 2. Let valid be the return value of set a configuration with
  // configuration and true on this.
  sanitizer->SetConfig(aConfig.GetAsSanitizerConfig(), true, aRv);

  // Step 3. If valid is false, then throw a TypeError.
  if (aRv.Failed()) {
    return nullptr;
  }

  return sanitizer.forget();
}

void Sanitizer::SetDefaultConfig() {
  MOZ_ASSERT(NS_IsMainThread());
  AssertNoLists();
  MOZ_ASSERT(!mComments);
  MOZ_ASSERT(mDataAttributes.isNothing());

  mIsDefaultConfig = true;

  // https://wicg.github.io/sanitizer-api/#built-in-safe-default-configuration
  // {
  //   ...
  //   "comments": false,
  //   "dataAttributes": false
  // }
  mComments = false;
  mDataAttributes = Some(false);

  if (sDefaultHTMLElements) {
    // Already initialized.
    return;
  }

  auto createElements = [](mozilla::Span<nsStaticAtom* const> aElements,
                           nsStaticAtom* const* aElementWithAttributes) {
    auto elements = new ElementsWithAttributes(aElements.Length());

    size_t i = 0;
    for (nsStaticAtom* name : aElements) {
      UniquePtr<StaticAtomSet> attributes = nullptr;

      // Walkthrough the element specific attribute list in lockstep.
      // The last "name" in the array is a nullptr sentinel.
      if (name == aElementWithAttributes[i]) {
        attributes = MakeUnique<StaticAtomSet>();
        while (aElementWithAttributes[++i]) {
          attributes->Insert(aElementWithAttributes[i]);
        }
        i++;
      }

      elements->InsertOrUpdate(name, std::move(attributes));
    }

    return elements;
  };

  sDefaultHTMLElements =
      createElements(Span(kDefaultHTMLElements), kHTMLElementWithAttributes);
  sDefaultMathMLElements = createElements(Span(kDefaultMathMLElements),
                                          kMathMLElementWithAttributes);
  sDefaultSVGElements =
      createElements(Span(kDefaultSVGElements), kSVGElementWithAttributes);

  sDefaultAttributes = new StaticAtomSet(std::size(kDefaultAttributes));
  for (nsStaticAtom* name : kDefaultAttributes) {
    sDefaultAttributes->Insert(name);
  }

  ClearOnShutdown(&sDefaultHTMLElements);
  ClearOnShutdown(&sDefaultMathMLElements);
  ClearOnShutdown(&sDefaultAttributes);
}

// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element
template <typename SanitizerElement>
static CanonicalElement CanonicalizeElement(const SanitizerElement& aElement) {
  // return the result of canonicalize a sanitizer name with element and the
  // HTML namespace as the default namespace.

  // https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-name
  // Step 1. Assert: name is either a DOMString or a dictionary. (implicit)

  // Step 2. If name is a DOMString, then return «[ "name" → name, "namespace"
  // → defaultNamespace]».
  if (aElement.IsString()) {
    RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(aElement.GetAsString());
    return CanonicalElement(nameAtom, nsGkAtoms::nsuri_xhtml);
  }

  // Step 3. Assert: name is a dictionary and both name["name"] and
  // name["namespace"] exist.
  const auto& elem = GetAsDictionary(aElement);
  MOZ_ASSERT(!elem.mName.IsVoid());

  // Step 4. If name["namespace"] is the empty string, then set it to null.
  RefPtr<nsAtom> namespaceAtom;
  if (!elem.mNamespace.IsEmpty()) {
    namespaceAtom = NS_AtomizeMainThread(elem.mNamespace);
  }

  // Step 5. Return «[
  //  "name" → name["name"],
  //  "namespace" → name["namespace"]
  //  )
  // ]».
  RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(elem.mName);
  return CanonicalElement(nameAtom, namespaceAtom);
}

// https://html.spec.whatwg.org/#canonicalize-a-processing-instruction
template <typename SanitizerPI>
static already_AddRefed<nsAtom> CanonicalizeProcessingInstruction(
    const SanitizerPI& aPI) {
  // Step 1. If pi is a DOMString, then return «[ "target" → pi ]».
  if (aPI.IsString()) {
    RefPtr<nsAtom> piAtom = NS_AtomizeMainThread(aPI.GetAsString());
    return piAtom.forget();
  }

  // Step 2. Assert: pi is a dictionary and pi["target"] exists.
  // Step 3. Return «[ "target" → pi["target"] ]».
  const auto& pi = GetAsDictionary(aPI);
  return NS_AtomizeMainThread(pi.mTarget);
}

// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-attribute
template <typename SanitizerAttribute>
static CanonicalAttribute CanonicalizeAttribute(
    const SanitizerAttribute& aAttribute) {
  // return the result of canonicalize a sanitizer name with attribute and
  // null as the default namespace.

  // https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-name
  // Step 1. Assert: name is either a DOMString or a dictionary. (implicit)

  // Step 2. If name is a DOMString, then return «[ "name" → name, "namespace"
  // → defaultNamespace]».
  if (aAttribute.IsString()) {
    RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(aAttribute.GetAsString());
    return CanonicalAttribute(nameAtom, nullptr);
  }

  // Step 3. Assert: name is a dictionary and both name["name"] and
  // name["namespace"] exist.
  const auto& attr = aAttribute.GetAsSanitizerAttributeNamespace();
  MOZ_ASSERT(!attr.mName.IsVoid());

  // Step 4. If name["namespace"] is the empty string, then set it to null.
  RefPtr<nsAtom> namespaceAtom;
  if (!attr.mNamespace.IsEmpty()) {
    namespaceAtom = NS_AtomizeMainThread(attr.mNamespace);
  }

  // Step 5. Return «[
  //  "name" → name["name"],
  //  "namespace" → name["namespace"],
  //  )
  // ]».
  RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(attr.mName);
  return CanonicalAttribute(nameAtom, namespaceAtom);
}

// https://wicg.github.io/sanitizer-api/#canonicalize-a-sanitizer-element-with-attributes
//
// If aErrorMsg is not nullptr, this function will abort for duplicate
// attributes and set an error message, but otherwise they are ignored.
template <typename SanitizerElementWithAttributes>
static CanonicalElementAttributes CanonicalizeElementAttributes(
    const SanitizerElementWithAttributes& aElement,
    nsACString* aErrorMsg = nullptr) {
  // Step 1. Let result be the result of canonicalize a sanitizer element with
  // element.
  //
  // NOTE: CanonicalizeElement is the responsibility of the caller.
  CanonicalElementAttributes result{};

  // Step 2. If element is a dictionary:
  if (aElement.IsSanitizerElementNamespaceWithAttributes()) {
    auto& elem = aElement.GetAsSanitizerElementNamespaceWithAttributes();

    // Step 2.1. If element["attributes"] exists:
    if (elem.mAttributes.WasPassed()) {
      // Step 2.1.1. Let attributes be « ».
      CanonicalAttributeSet attributes;

      // Step 2.1.2. For each attribute of element["attributes"]:
      for (const auto& attribute : elem.mAttributes.Value()) {
        // Step 2.1.2.1. Append the result of canonicalize a sanitizer attribute
        // with attribute to attributes.
        CanonicalAttribute canonicalAttr = CanonicalizeAttribute(attribute);
        if (!attributes.EnsureInserted(canonicalAttr)) {
          if (aErrorMsg) {
            aErrorMsg->Assign(
                nsFmtCString("Duplicate attribute {} in 'attributes' of {}.",
                             canonicalAttr, CanonicalizeElement(aElement)));
            return CanonicalElementAttributes();
          }
        }
      }

      // Step 2.1.3. Set result["attributes"] to attributes.
      result.mAttributes = Some(std::move(attributes));
    }

    // Step 2.2. If element["attributes"] exists:
    if (elem.mRemoveAttributes.WasPassed()) {
      // Step 2.2.1. Let attributes be « ».
      CanonicalAttributeSet attributes;

      // Step 2.2.2. For each attribute of element["removeAttributes"]:
      for (const auto& attribute : elem.mRemoveAttributes.Value()) {
        // Step 2.2.2.1. Append the result of canonicalize a sanitizer attribute
        // with attribute to attributes.
        CanonicalAttribute canonicalAttr = CanonicalizeAttribute(attribute);
        if (!attributes.EnsureInserted(canonicalAttr)) {
          if (aErrorMsg) {
            aErrorMsg->Assign(nsFmtCString(
                "Duplicate attribute {} in 'removeAttributes' of {}.",
                canonicalAttr, CanonicalizeElement(aElement)));
            return CanonicalElementAttributes();
          }
        }
      }

      // Step 2.2.3. Set result["removeAttributes"] to attributes.
      result.mRemoveAttributes = Some(std::move(attributes));
    }
  }

  // Step 3. If neither result["attributes"] nor
  // result["removeAttributes"] exist:
  if (!result.mAttributes && !result.mRemoveAttributes) {
    // Step 3.1. Set result["removeAttributes"] to « ».
    CanonicalAttributeSet set{};
    result.mRemoveAttributes = Some(std::move(set));
  }

  // Step 4. Return result.
  return result;
}

// https://html.spec.whatwg.org/#canonicalize-the-configuration
void Sanitizer::CanonicalizeConfiguration(
    const SanitizerConfig& aConfig, bool aAllowCommentsPIsAndDataAttributes,
    ErrorResult& aRv) {
  // This function is only called while constructing a new Sanitizer object.
  AssertNoLists();

  // Step 1. If neither configuration["elements"] nor
  // configuration["removeElements"] exists, then set
  // configuration["removeElements"] to an empty list.
  if (!aConfig.mElements.WasPassed() && !aConfig.mRemoveElements.WasPassed()) {
    mRemoveElements.emplace();
  }

  // Step 2. If neither configuration["attributes"] nor
  // configuration["removeAttributes"] exists, then set
  // configuration["removeAttributes"] to an empty list.
  if (!aConfig.mAttributes.WasPassed() &&
      !aConfig.mRemoveAttributes.WasPassed()) {
    mRemoveAttributes.emplace();
  }

  // Step 3. If neither configuration["processingInstructions"] nor
  // configuration["removeProcessingInstructions"] exists:
  if (!aConfig.mProcessingInstructions.WasPassed() &&
      !aConfig.mRemoveProcessingInstructions.WasPassed()) {
    // Step 3.1. If allowCommentsPIsAndDataAttributes is true, then set
    // configuration["removeProcessingInstructions"] to an empty list.
    if (aAllowCommentsPIsAndDataAttributes) {
      mRemoveProcessingInstructions.emplace();
    } else {
      // Step 3.2. Otherwise, set configuration["processingInstructions"] to an
      // empty list.
      mProcessingInstructions.emplace();
    }
  }

  // Step 4. If configuration["elements"] exists:
  if (aConfig.mElements.WasPassed()) {
    // Step 4.1. Let newElements be « ».
    CanonicalElementMap elements;

    nsAutoCString errorMsg;
    // Step 4.2. For each element of configuration["elements"], append the
    // result of canonicalizing element to newElements.
    for (const auto& element : aConfig.mElements.Value()) {
      CanonicalElement elementName = CanonicalizeElement(element);
      if (elements.Contains(elementName)) {
        aRv.ThrowTypeError(
            nsFmtCString("Duplicate element {} in 'elements'.", elementName));
        return;
      }

      CanonicalElementAttributes elementAttributes =
          CanonicalizeElementAttributes(element, &errorMsg);
      if (!errorMsg.IsEmpty()) {
        aRv.ThrowTypeError(errorMsg);
        return;
      }

      elements.InsertOrUpdate(elementName, std::move(elementAttributes));
    }

    // Step 4.3. Set configuration["elements"] to newElements.
    mElements = Some(std::move(elements));
  }

  // Step 5. If configuration["removeElements"] exists, then set
  // configuration["removeElements"] to the result of canonicalizing
  // configuration["removeElements"].
  if (aConfig.mRemoveElements.WasPassed()) {
    CanonicalElementSet elements;
    for (const auto& element : aConfig.mRemoveElements.Value()) {
      CanonicalElement canonical = CanonicalizeElement(element);
      if (!elements.EnsureInserted(canonical)) {
        aRv.ThrowTypeError(nsFmtCString(
            "Duplicate element {} in 'removeElements'.", canonical));
        return;
      }
    }
    mRemoveElements = Some(std::move(elements));
  }

  // Step 6. If configuration["attributes"] exists, then set
  // configuration["attributes"] to the result of canonicalizing
  // configuration["attributes"].
  if (aConfig.mAttributes.WasPassed()) {
    CanonicalAttributeSet attributes;
    for (const auto& attribute : aConfig.mAttributes.Value()) {
      CanonicalAttribute canonical = CanonicalizeAttribute(attribute);
      if (!attributes.EnsureInserted(canonical)) {
        aRv.ThrowTypeError(
            nsFmtCString("Duplicate attribute {} in 'attributes'.", canonical));
        return;
      }
    }
    mAttributes = Some(std::move(attributes));
  }

  // Step 7. If configuration["removeAttributes"] exists, then set
  // configuration["removeAttributes"] to the result of canonicalizing
  // configuration["removeAttributes"].
  if (aConfig.mRemoveAttributes.WasPassed()) {
    CanonicalAttributeSet attributes;
    for (const auto& attribute : aConfig.mRemoveAttributes.Value()) {
      CanonicalAttribute canonical = CanonicalizeAttribute(attribute);
      if (!attributes.EnsureInserted(canonical)) {
        aRv.ThrowTypeError(nsFmtCString(
            "Duplicate attribute {} in 'removeAttributes'.", canonical));
        return;
      }
    }
    mRemoveAttributes = Some(std::move(attributes));
  }

  // Step 8. If configuration["replaceWithChildrenElements"] exists, then set
  // configuration["replaceWithChildrenElements"] to the result of
  // canonicalizing configuration["replaceWithChildrenElements"].
  if (aConfig.mReplaceWithChildrenElements.WasPassed()) {
    CanonicalElementSet elements;
    for (const auto& element : aConfig.mReplaceWithChildrenElements.Value()) {
      CanonicalElement canonical = CanonicalizeElement(element);
      if (!elements.EnsureInserted(canonical)) {
        aRv.ThrowTypeError(nsFmtCString(
            "Duplicate element {} in 'replaceWithChildrenElements'.",
            canonical));
        return;
      }
    }
    mReplaceWithChildrenElements = Some(std::move(elements));
  }

  // Step 9. If configuration["processingInstructions"] exists, then set
  // configuration["processingInstructions"] to the result of canonicalizing
  // configuration["processingInstructions"].
  if (aConfig.mProcessingInstructions.WasPassed()) {
    CanonicalPISet pis;
    for (const auto& pi : aConfig.mProcessingInstructions.Value()) {
      RefPtr<nsAtom> canonical = CanonicalizeProcessingInstruction(pi);
      if (!pis.EnsureInserted(canonical)) {
        nsAutoCString name;
        canonical->ToUTF8String(name);
        aRv.ThrowTypeError(
            nsFmtCString("Duplicate processing instruction \"{}\" in "
                         "'processingInstructions'.",
                         name));
        return;
      }
    }
    mProcessingInstructions = Some(std::move(pis));
  }

  // Step 10. If configuration["removeProcessingInstructions"] exists, then set
  // configuration["removeProcessingInstructions"] to the result of
  // canonicalizing configuration["removeProcessingInstructions"].
  if (aConfig.mRemoveProcessingInstructions.WasPassed()) {
    CanonicalPISet pis;
    for (const auto& pi : aConfig.mRemoveProcessingInstructions.Value()) {
      RefPtr<nsAtom> canonical = CanonicalizeProcessingInstruction(pi);
      if (!pis.EnsureInserted(canonical)) {
        nsAutoCString name;
        canonical->ToUTF8String(name);
        aRv.ThrowTypeError(
            nsFmtCString("Duplicate processing instruction \"{}\" in "
                         "'removeProcessingInstructions'.",
                         name));
        return;
      }
    }
    mRemoveProcessingInstructions = Some(std::move(pis));
  }

  // Step 11. If configuration["comments"] does not exist, then set it to
  // allowCommentsPIsAndDataAttributes.
  if (aConfig.mComments.WasPassed()) {
    // NOTE: We always need to copy this property if it exists.
    mComments = aConfig.mComments.Value();
  } else {
    mComments = aAllowCommentsPIsAndDataAttributes;
  }

  // Step 12. If configuration["attributes"] exists and
  // configuration["dataAttributes"] does not exist, then set it to
  // allowCommentsPIsAndDataAttributes.
  if (aConfig.mDataAttributes.WasPassed()) {
    // NOTE: We always need to copy this property if it exists.
    mDataAttributes = Some(aConfig.mDataAttributes.Value());
  } else if (aConfig.mAttributes.WasPassed()) {
    mDataAttributes = Some(aAllowCommentsPIsAndDataAttributes);
  }
}

// https://wicg.github.io/sanitizer-api/#built-in-non-replaceable-elements-list
// The built-in non-replaceable elements list
static bool IsNonReplaceableElement(const CanonicalElement& aElement) {
  return aElement ==
             CanonicalElement(nsGkAtoms::html, nsGkAtoms::nsuri_xhtml) ||
         aElement == CanonicalElement(nsGkAtoms::svg, nsGkAtoms::nsuri_svg) ||
         aElement == CanonicalElement(nsGkAtoms::math, nsGkAtoms::nsuri_mathml);
}

// https://wicg.github.io/sanitizer-api/#sanitizerconfig-valid
void Sanitizer::IsValid(ErrorResult& aRv) {
  // Step 1. The config has either an elements or a removeElements key, but
  // not both.
  MOZ_ASSERT(mElements || mRemoveElements,
             "Must have either due to CanonicalizeConfiguration");
  if (mElements && mRemoveElements) {
    aRv.ThrowTypeError(
        "'elements' and 'removeElements' are not allowed at the same time.");
    return;
  }

  // Assert: Either config["processingInstructions"] exists or
  // config["removeProcessingInstructions"] exists.
  MOZ_ASSERT(mProcessingInstructions || mRemoveProcessingInstructions);
  // If config["processingInstructions"] exists and
  // config["removeProcessingInstructions"] exists, then return false.
  if (mProcessingInstructions && mRemoveProcessingInstructions) {
    aRv.ThrowTypeError(
        "'processingInstructions' and 'removeProcessingInstructions' are not "
        "allowed at the same time.");
    return;
  }

  // Step 2. The config has either an attributes or a removeAttributes key,
  // but not both.
  MOZ_ASSERT(mAttributes || mRemoveAttributes,
             "Must have either due to CanonicalizeConfiguration");
  if (mAttributes && mRemoveAttributes) {
    aRv.ThrowTypeError(
        "'attributes' and 'removeAttributes' are not allowed at the same "
        "time.");
    return;
  }

  // Step 3. Assert: All SanitizerElementNamespaceWithAttributes,
  // SanitizerElementNamespace, and SanitizerAttributeNamespace items in
  // config are canonical, meaning they have been run through canonicalize a
  // sanitizer element or canonicalize a sanitizer attribute, as appropriate.

  // Step 4. None of config[elements], config[removeElements],
  // config[replaceWithChildrenElements], config[attributes], or
  // config[removeAttributes], if they exist, has duplicates.
  //
  // NOTE: The Sanitizer::CanonicalizeConfiguration method would have already
  // thrown an error for duplicate elements/attributes by this point. The map
  // and sets can't have duplicates by definition.

  // Step 11. If config["replaceWithChildrenElements"] exists:
  if (mReplaceWithChildrenElements) {
    // Step 11.1. For each element of config["replaceWithChildrenElements"]:
    for (const CanonicalElement& element : *mReplaceWithChildrenElements) {
      // Step 11.1.1. If the built-in non-replaceable elements list contains
      // element, then return false.
      if (IsNonReplaceableElement(element)) {
        aRv.ThrowTypeError(nsFmtCString(
            "Element {} is not allowed in 'replaceWithChildrenElements'",
            element));
        return;
      }
    }

    // Step 11.2. If config["elements"] exists:
    if (mElements) {
      // Step 11.2.1. If the intersection of config["elements"] and
      // config["replaceWithChildrenElements"] is not empty, then return false.
      for (const CanonicalElement& name : mElements->Keys()) {
        if (mReplaceWithChildrenElements->Contains(name)) {
          aRv.ThrowTypeError(
              nsFmtCString("Element {} can't be in both 'elements' "
                           "and 'replaceWithChildrenElements'.",
                           name));
          return;
        }
      }
    } else {
      // Step 11.3. Otherwise:
      // Step 11.3.1. If the intersection of config["removeElements"] and
      // config["replaceWithChildrenElements"] is not empty, then return false.
      for (const CanonicalElement& name : *mRemoveElements) {
        if (mReplaceWithChildrenElements->Contains(name)) {
          aRv.ThrowTypeError(
              nsFmtCString("Element {} can't be in both 'removeElements' and "
                           "'replaceWithChildrenElements'.",
                           name));
          return;
        }
      }
    }
  }

  // Step 7. If config[attributes] exists:
  if (mAttributes) {
    // Step 7.1. If config[elements] exists:
    if (mElements) {
      // Step 7.1.1 For any element in config[elements]:
      for (const auto& entry : *mElements) {
        const CanonicalElementAttributes& elemAttributes = entry.GetData();
        MOZ_ASSERT(
            elemAttributes.mAttributes || elemAttributes.mRemoveAttributes,
            "Canonical elements must at least have removeAttributes");

        // Step 7.1.1.1. Neither element[attributes] or
        // element[removeAttributes], if they exist, has duplicates.
        //
        // NOTE: The Sanitizer::CanonicalizeConfiguration (specifically
        // CanonicalizeElementAttributes) method would have already thrown an
        // error for duplicate attributes by this point. The attribute sets
        // can't have duplicates by definition.

        // Step 7.1.1.2. The intersection of config[attributes] and
        // element[attributes] with default « [] » is empty.
        if (elemAttributes.mAttributes) {
          for (const CanonicalAttribute& name : *elemAttributes.mAttributes) {
            if (mAttributes->Contains(name)) {
              aRv.ThrowTypeError(nsFmtCString(
                  "Attribute {} can't be part of both the 'attributes' of "
                  "the element {} and the global 'attributes'.",
                  name, entry.GetKey()));
              return;
            }
          }
        }

        // Step 7.1.1.3. element[removeAttributes] is a subset of
        // config[attributes].
        if (elemAttributes.mRemoveAttributes) {
          for (const CanonicalAttribute& name :
               *elemAttributes.mRemoveAttributes) {
            if (!mAttributes->Contains(name)) {
              aRv.ThrowTypeError(nsFmtCString(
                  "Attribute {} can't be in 'removeAttributes' of the "
                  "element {} but not in the global 'attributes'.",
                  name, entry.GetKey()));
              return;
            }
          }
        }

        MOZ_ASSERT(mDataAttributes.isSome(),
                   "mDataAttributes exists iff mAttributes exists");

        // Step 7.1.1.4. If dataAttributes is true:
        if (*mDataAttributes && elemAttributes.mAttributes) {
          // TODO: Merge with loop above?
          // Step 7.1.1.4.1. element[attributes] does not contain a custom
          // data attribute.
          for (const CanonicalAttribute& name : *elemAttributes.mAttributes) {
            if (name.IsDataAttribute()) {
              aRv.ThrowTypeError(nsFmtCString(
                  "Data attribute {} in the 'attributes' of the element {} "
                  "is redundant with 'dataAttributes' being true.",
                  name, entry.GetKey()));
              return;
            }
          }
        }
      }
    }

    MOZ_ASSERT(mDataAttributes.isSome(),
               "mDataAttributes exists iff mAttributes exists");

    // Step 7.2. If dataAttributes is true:
    if (*mDataAttributes) {
      // Step 7.2.1. config[attributes] does not contain a custom data
      // attribute.
      for (const CanonicalAttribute& name : *mAttributes) {
        if (name.IsDataAttribute()) {
          aRv.ThrowTypeError(
              nsFmtCString("Data attribute {} in the global 'attributes' is "
                           "redundant with 'dataAttributes' being true.",
                           name));
          return;
        }
      }
    }
  }

  // Step 8. If config[removeAttributes] exists:
  if (mRemoveAttributes) {
    // Step 8.1. If config[elements] exists, then for any element in
    // config[elements]:
    if (mElements) {
      for (const auto& entry : *mElements) {
        const CanonicalElementAttributes& elemAttributes = entry.GetData();

        // Step 8.1.1. Not both element[attributes] and
        // element[removeAttributes] exist.
        if (elemAttributes.mAttributes && elemAttributes.mRemoveAttributes) {
          return aRv.ThrowTypeError(
              nsFmtCString("Element {} can't have both 'attributes' "
                           "and 'removeAttributes'.",
                           entry.GetKey()));
        }

        // Step 8.1.2. Neither element[attributes] nor
        // element[removeAttributes], if they exist, has duplicates.
        //
        // NOTE: The Sanitizer::CanonicalizeConfiguration (specifically
        // CanonicalizeElementAttributes) method would have already thrown an
        // error for duplicate attributes by this point. The attribute sets
        // can't have duplicates by definition.

        // Step 8.1.3. The intersection of config[removeAttributes] and
        // element[attributes] with default « [] » is empty.
        if (elemAttributes.mAttributes) {
          for (const CanonicalAttribute& name : *elemAttributes.mAttributes) {
            if (mRemoveAttributes->Contains(name)) {
              aRv.ThrowTypeError(nsFmtCString(
                  "Attribute {} can't be in 'attributes' of the element {} "
                  "while in the global 'removeAttributes'.",
                  name, entry.GetKey()));
              return;
            }
          }
        }

        // Step 8.1.4. The intersection of config[removeAttributes] and
        // element[removeAttributes] with default « [] » is empty.
        if (elemAttributes.mRemoveAttributes) {
          for (const CanonicalAttribute& name :
               *elemAttributes.mRemoveAttributes) {
            if (mRemoveAttributes->Contains(name)) {
              aRv.ThrowTypeError(
                  nsFmtCString("Attribute {} can't be part of both the "
                               "'removeAttributes' of the element {} and the "
                               "global 'removeAttributes'.",
                               name, entry.GetKey()));
              return;
            }
          }
        }
      }
    }

    // Step 8.2. config[dataAttributes] does not exist.
    if (mDataAttributes) {
      aRv.ThrowTypeError(
          "'removeAttributes' and 'dataAttributes' aren't allowed at the "
          "same time.");
    }
  }
}

void Sanitizer::AssertIsValid() {
#ifdef DEBUG
  IgnoredErrorResult rv;
  IsValid(rv);
  MOZ_ASSERT(!rv.Failed());
#endif
}

// https://html.spec.whatwg.org/#configure-a-sanitizer
void Sanitizer::SetConfig(const SanitizerConfig& aConfig,
                          bool aAllowCommentsPIsAndDataAttributes,
                          ErrorResult& aRv) {
  // Step 1. Canonicalize configuration with allowCommentsPIsAndDataAttributes.
  CanonicalizeConfiguration(aConfig, aAllowCommentsPIsAndDataAttributes, aRv);
  if (aRv.Failed()) {
    return;
  }

  // Step 2. If configuration is not valid, then throw a TypeError.
  IsValid(aRv);
  if (aRv.Failed()) {
    return;
  }

  // Step 3. Set sanitizer's configuration to configuration.
  // Note: This was already done in CanonicalizeConfiguration.
}

// Turn the lazy default config into real lists that can be
// modified or queried via get().
void Sanitizer::MaybeMaterializeDefaultConfig() {
  if (!mIsDefaultConfig) {
    AssertIsValid();
    return;
  }

  AssertNoLists();

  CanonicalElementMap elements;
  auto insertElements = [&elements](
                            mozilla::Span<nsStaticAtom* const> aElements,
                            nsStaticAtom* aNamespace,
                            nsStaticAtom* const* aElementWithAttributes) {
    size_t i = 0;
    for (nsStaticAtom* name : aElements) {
      CanonicalElementAttributes elementAttributes{};

      if (name == aElementWithAttributes[i]) {
        CanonicalAttributeSet attributes;
        while (aElementWithAttributes[++i]) {
          attributes.Insert(
              CanonicalAttribute(aElementWithAttributes[i], nullptr));
        }
        i++;
        elementAttributes.mAttributes = Some(std::move(attributes));
      } else {
        // In the default config all elements have a (maybe empty) `attributes`
        // list.
        CanonicalAttributeSet set{};
        elementAttributes.mAttributes = Some(std::move(set));
      }

      CanonicalElement elementName(name, aNamespace);
      elements.InsertOrUpdate(elementName, std::move(elementAttributes));
    }
  };
  insertElements(Span(kDefaultHTMLElements), nsGkAtoms::nsuri_xhtml,
                 kHTMLElementWithAttributes);
  insertElements(Span(kDefaultMathMLElements), nsGkAtoms::nsuri_mathml,
                 kMathMLElementWithAttributes);
  insertElements(Span(kDefaultSVGElements), nsGkAtoms::nsuri_svg,
                 kSVGElementWithAttributes);
  mElements = Some(std::move(elements));

  mProcessingInstructions = Some(CanonicalPISet{});

  CanonicalAttributeSet attributes;
  for (nsStaticAtom* name : kDefaultAttributes) {
    attributes.Insert(CanonicalAttribute(name, nullptr));
  }
  mAttributes = Some(std::move(attributes));

  mIsDefaultConfig = false;
}

// https://wicg.github.io/sanitizer-api/#dom-sanitizer-get
void Sanitizer::Get(SanitizerConfig& aConfig) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Let config be this’s configuration.
  // Step 2. If config["elements"] exists:
  if (mElements) {
    nsTArray<OwningStringOrSanitizerElementNamespaceWithAttributes> elements;
    // Step 2.1. For any element of config["elements"]:
    for (const auto& entry : *mElements) {
      // Step 2.1.1. If element["attributes"] exists:
      // Step 2.1.2. If element["removeAttributes"] exists:
      // ...
      // (The attributes are sorted by the ToSanitizerAttributes call in
      // ToSanitizerElementNamespaceWithAttributes)
      OwningStringOrSanitizerElementNamespaceWithAttributes owning;
      owning.SetAsSanitizerElementNamespaceWithAttributes() =
          entry.GetKey().ToSanitizerElementNamespaceWithAttributes(
              entry.GetData());

      // Step 2.2. Set config["elements"] to the result of sort in ascending
      // order config["elements"], with elementA being less than item elementB.
      // (Instead of sorting at the end, we sort during insertion)
      elements.InsertElementSorted(owning,
                                   SanitizerComparator<decltype(owning)>());
    }
    aConfig.mElements.Construct(std::move(elements));
  } else {
    // Step 3. If config["removeElements"] exists:
    // Step 3.1. Set config["removeElements"] to the result of sort in ascending
    // order config["removeElements"], with elementA being less than item
    // elementB.
    nsTArray<OwningStringOrSanitizerElementNamespace> removeElements;
    for (const CanonicalElement& canonical : *mRemoveElements) {
      OwningStringOrSanitizerElementNamespace owning;
      owning.SetAsSanitizerElementNamespace() =
          canonical.ToSanitizerElementNamespace();
      removeElements.InsertElementSorted(
          owning, SanitizerComparator<decltype(owning)>());
    }
    aConfig.mRemoveElements.Construct(std::move(removeElements));
  }

  // Step 4. If config["replaceWithChildrenElements"] exists:
  if (mReplaceWithChildrenElements) {
    // Step 4.1. Set config["replaceWithChildrenElements"] to the result of sort
    // in ascending order config["replaceWithChildrenElements"], with elementA
    // being less than item elementB.
    nsTArray<OwningStringOrSanitizerElementNamespace>
        replaceWithChildrenElements;
    for (const CanonicalElement& canonical : *mReplaceWithChildrenElements) {
      OwningStringOrSanitizerElementNamespace owning;
      owning.SetAsSanitizerElementNamespace() =
          canonical.ToSanitizerElementNamespace();
      replaceWithChildrenElements.InsertElementSorted(
          owning, SanitizerComparator<decltype(owning)>());
    }
    aConfig.mReplaceWithChildrenElements.Construct(
        std::move(replaceWithChildrenElements));
  }

  if (mProcessingInstructions) {
    nsTArray<OwningStringOrSanitizerProcessingInstruction>
        processingInstructions;
    for (const RefPtr<nsAtom>& canonical : *mProcessingInstructions) {
      SanitizerProcessingInstruction pi;
      canonical->ToString(pi.mTarget);
      OwningStringOrSanitizerProcessingInstruction owning;
      owning.SetAsSanitizerProcessingInstruction() = pi;
      processingInstructions.InsertElementSorted(owning, PIComparator());
    }
    aConfig.mProcessingInstructions.Construct(
        std::move(processingInstructions));
  } else {
    nsTArray<OwningStringOrSanitizerProcessingInstruction>
        removeProcessingInstructions;
    for (const RefPtr<nsAtom>& canonical : *mRemoveProcessingInstructions) {
      SanitizerProcessingInstruction pi;
      canonical->ToString(pi.mTarget);
      OwningStringOrSanitizerProcessingInstruction owning;
      owning.SetAsSanitizerProcessingInstruction() = pi;
      removeProcessingInstructions.InsertElementSorted(owning, PIComparator());
    }
    aConfig.mRemoveProcessingInstructions.Construct(
        std::move(removeProcessingInstructions));
  }

  // Step 5. If config["attributes"] exists:
  if (mAttributes) {
    // Step 5.1. Set config["attributes"] to the result of sort in ascending
    // order config["attributes"], with attrA being less than item attrB.
    // (Sorting is done by ToSanitizerAttributes)
    aConfig.mAttributes.Construct(ToSanitizerAttributes(*mAttributes));
  } else {
    // Step 6. If config["removeAttributes"] exists:
    // Step 6.1. Set config["removeAttributes"] to the result of sort in
    // ascending order config["removeAttributes"], with attrA being less than
    // item attrB.
    aConfig.mRemoveAttributes.Construct(
        ToSanitizerAttributes(*mRemoveAttributes));
  }

  // (In the spec these already exist in the |config| and don't need sorting)
  aConfig.mComments.Construct(mComments);
  if (mDataAttributes) {
    aConfig.mDataAttributes.Construct(*mDataAttributes);
  }

  // Step 7. Return config.
}

// https://wicg.github.io/sanitizer-api/#sanitizerconfig-allow-an-element
bool Sanitizer::AllowElement(
    const StringOrSanitizerElementNamespaceWithAttributes& aElement) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set element to the result of canonicalize a sanitizer element
  // with attributes with element.
  CanonicalElement elementName = CanonicalizeElement(aElement);
  // NOTE: Duplicate attributes are removed/ignored.
  CanonicalElementAttributes elementAttributes =
      CanonicalizeElementAttributes(aElement);

  // Step 2 If configuration["elements"] exists:
  if (mElements) {
    // Step 2.1. Set modified to the result of remove element from
    // configuration["replaceWithChildrenElements"].
    bool modified =
        mReplaceWithChildrenElements
            ? mReplaceWithChildrenElements->EnsureRemoved(elementName)
            : false;

    // Step 2.2. Comment: We need to make sure the per-element attributes do
    // not overlap with global attributes.

    // Step 2.3. If configuration["attributes"] exists:
    if (mAttributes) {
      // Step 2.3.1. If element["attributes"] exists:
      if (elementAttributes.mAttributes) {
        CanonicalAttributeSet attributes;
        for (const CanonicalAttribute& attr : *elementAttributes.mAttributes) {
          // Step 2.3.1.1. Set element["attributes"] to remove duplicates from
          // element["attributes"].
          MOZ_ASSERT(!attributes.Contains(attr));

          // Step 2.3.1.2. Set element["attributes"] to the difference of
          // element["attributes"] and configuration["attributes"].
          if (mAttributes->Contains(attr)) {
            continue;
          }

          // Step 2.3.1.3. If configuration["dataAttributes"] is true:
          MOZ_ASSERT(mDataAttributes.isSome(),
                     "mDataAttributes exists iff mAttributes");
          if (*mDataAttributes) {
            // Step 2.3.1.3.1 Remove all items item from element["attributes"]
            // where item is a custom data attribute.
            if (attr.IsDataAttribute()) {
              continue;
            }
          }

          attributes.Insert(attr.Clone());
        }
        elementAttributes.mAttributes = Some(std::move(attributes));
      }

      // Step 2.3.2. If element["removeAttributes"] exists:
      if (elementAttributes.mRemoveAttributes) {
        CanonicalAttributeSet removeAttributes;
        for (const CanonicalAttribute& attr :
             *elementAttributes.mRemoveAttributes) {
          // Step 2.3.2.1. Set element["removeAttributes"] to remove duplicates
          // from element["removeAttributes"].
          //
          // NOTE: CanonicalizeElementAttributes removed all duplicates for us.
          MOZ_ASSERT(!removeAttributes.Contains(attr));

          // Step 2.3.2.2. Set element["removeAttributes"] to the intersection
          // of element["removeAttributes"] and configuration["attributes"].
          if (!mAttributes->Contains(attr)) {
            continue;
          }

          removeAttributes.Insert(attr.Clone());
        }
        elementAttributes.mRemoveAttributes = Some(std::move(removeAttributes));
      }
    } else {
      // Step 2.4. Otherwise:

      // Step 2.4.1. If element["attributes"] exists:
      if (elementAttributes.mAttributes) {
        CanonicalAttributeSet attributes;
        for (const CanonicalAttribute& attr : *elementAttributes.mAttributes) {
          // Step 2.4.1.1. Set element["attributes"] to remove duplicates from
          // element["attributes"].
          //
          // NOTE: CanonicalizeElementAttributes removed all duplicates for us.
          MOZ_ASSERT(!attributes.Contains(attr));

          // Step 2.4.1.2. Set element["attributes"] to the difference of
          // element["attributes"] and element["removeAttributes"] with default
          // « ».
          if (elementAttributes.mRemoveAttributes &&
              elementAttributes.mRemoveAttributes->Contains(attr)) {
            continue;
          }

          // Step 2.4.1.4. Set element["attributes"] to the difference of
          // element["attributes"] and configuration["removeAttributes"].
          if (mRemoveAttributes->Contains(attr)) {
            continue;
          }

          attributes.Insert(attr.Clone());
        }
        elementAttributes.mAttributes = Some(std::move(attributes));

        // Step 2.4.1.3. Remove element["removeAttributes"].
        elementAttributes.mRemoveAttributes = Nothing();
      }

      // Step 2.4.2. If element["removeAttributes"] exists:
      if (elementAttributes.mRemoveAttributes) {
        CanonicalAttributeSet removeAttributes;
        for (const CanonicalAttribute& attr :
             *elementAttributes.mRemoveAttributes) {
          // Step 2.4.2.1. Set element["removeAttributes"] to remove duplicates
          // from element["removeAttributes"].
          MOZ_ASSERT(!removeAttributes.Contains(attr));

          // Step 2.4.2.2. Set element["removeAttributes"] to the difference of
          // element["removeAttributes"] and configuration["removeAttributes"].
          if (mRemoveAttributes->Contains(attr)) {
            continue;
          }

          removeAttributes.Insert(attr.Clone());
        }
        elementAttributes.mRemoveAttributes = Some(std::move(removeAttributes));
      }
    }

    // Step 2.5. If configuration["elements"] does not contain element:
    const CanonicalElementAttributes* existingElementAttributes =
        mElements->Lookup(elementName).DataPtrOrNull();
    if (!existingElementAttributes) {
      // Step 2.5.1. Comment: This is the case with a global allow-list that
      // does not yet contain element.

      // Step 2.5.2. Append element to configuration["elements"].
      mElements->InsertOrUpdate(elementName, std::move(elementAttributes));

      // Step 2.5.3. Return true.
      return true;
    }

    // Step 2.6. Comment: This is the case with a global allow-list that
    // already contains element.

    // Step 2.7. Let current element be the item in configuration["elements"]
    // where item[name] equals element[name] and item[namespace] equals
    // element[namespace]. (implict with existingElementAttributes)

    // Step 2.8. If element equals current element then return modified.
    if (elementAttributes.Equals(*existingElementAttributes)) {
      return modified;
    }

    // Step 2.9. Remove element from configuration["elements"].
    // Step 2.10. Append element to configuration["elements"].
    mElements->InsertOrUpdate(elementName, std::move(elementAttributes));

    // Step 2.11. Return true.
    return true;
  }

  // Step 3. Otherwise:
  // Step 3.1. If element["attributes"] exists or element["removeAttributes"]
  // with default « » is not empty:
  if (elementAttributes.mAttributes ||
      (elementAttributes.mRemoveAttributes &&
       !elementAttributes.mRemoveAttributes->IsEmpty())) {
    // Step 3.1.1. The user agent may report a warning to the console that this
    // operation is not supported.
    if (auto* win = mGlobal->GetAsInnerWindow()) {
      nsContentUtils::ReportToConsole(
          nsIScriptError::warningFlag, "Sanitizer"_ns, win->GetDoc(),
          PropertiesFile::SECURITY_PROPERTIES, "SanitizerAllowElementIgnored2");
    }

    // Step 3.1.2. Return false.
    return false;
  }

  // Step 3.2. Set modified to the result of remove element from
  // configuration["replaceWithChildrenElements"].
  bool modified = mReplaceWithChildrenElements
                      ? mReplaceWithChildrenElements->EnsureRemoved(elementName)
                      : false;

  // Step 3.3. If configuration["removeElements"] does not contain element:
  if (!mRemoveElements->Contains(elementName)) {
    // Step 3.3.1. Comment: This is the case with a global remove-list that
    // does not contain element.

    // Step 3.3.2. Return modified.
    return modified;
  }

  // Step 3.4. Comment: This is the case with a global remove-list that
  // contains element.

  // Step 3.5. Remove element from configuration["removeElements"].
  mRemoveElements->Remove(elementName);

  // Step 3.6. Return true.
  return true;
}

// https://wicg.github.io/sanitizer-api/#sanitizer-remove-an-element
bool Sanitizer::RemoveElement(
    const StringOrSanitizerElementNamespace& aElement) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set element to the result of canonicalize a sanitizer element
  // with element.
  CanonicalElement element = CanonicalizeElement(aElement);

  return RemoveElementCanonical(std::move(element));
}

bool Sanitizer::RemoveElementCanonical(CanonicalElement&& aElement) {
  // Step 2. Set modified to the result of remove element from
  // configuration["replaceWithChildrenElements"].
  bool modified = mReplaceWithChildrenElements
                      ? mReplaceWithChildrenElements->EnsureRemoved(aElement)
                      : false;

  // Step 3. If configuration["elements"] exists:
  if (mElements) {
    // Step 3.1. If configuration["elements"] contains element:
    if (mElements->Contains(aElement)) {
      // Step 3.1.1. Comment: We have a global allow list and it contains
      // element.

      // Step 3.1.2. Remove element from configuration["elements"].
      mElements->Remove(aElement);

      // Step 3.1.3. Return true.
      return true;
    }

    // Step 3.2. Comment: We have a global allow list and it does not contain
    // element.

    // Step 3.3. Return modified.
    return modified;
  }

  // Step 4. Otherwise:
  // Step 4.1. If configuration["removeElements"] contains element:
  if (mRemoveElements->Contains(aElement)) {
    // Step 4.1.1. Comment: We have a global remove list and it already
    // contains element.

    // Step 4.1.2. Return modified.
    return modified;
  }

  // Step 4.2. Comment: We have a global remove list and it does not contain
  // element.

  // Step 4.3. Add element to configuration["removeElements"].
  mRemoveElements->Insert(std::move(aElement));

  // Step 4.4. Return true.
  return true;
}

// https://wicg.github.io/sanitizer-api/#sanitizer-replace-an-element-with-its-children
bool Sanitizer::ReplaceElementWithChildren(
    const StringOrSanitizerElementNamespace& aElement) {
  // Step 1. Let configuration be this’s configuration.
  // Step 2. Assert: configuration is valid.
  MaybeMaterializeDefaultConfig();

  // Step 3. Set element to the result of canonicalize a sanitizer element with
  // element.
  CanonicalElement element = CanonicalizeElement(aElement);

  // Step 4. If the built-in non-replaceable elements list contains element:
  if (IsNonReplaceableElement(element)) {
    // Step 4.1. Return false.
    return false;
  }

  // Step 5. If configuration["replaceWithChildrenElements"] contains element:
  if (mReplaceWithChildrenElements &&
      mReplaceWithChildrenElements->Contains(element)) {
    // Step 5.1. Return false.
    return false;
  }

  // Step 6. Remove element from configuration["removeElements"].
  if (mRemoveElements) {
    mRemoveElements->Remove(element);
  } else {
    // Step 7. Remove element from configuration["elements"] list.
    mElements->Remove(element);
  }

  // Step 8. Add element to configuration["replaceWithChildrenElements"].
  if (!mReplaceWithChildrenElements) {
    mReplaceWithChildrenElements.emplace();
  }
  mReplaceWithChildrenElements->Insert(std::move(element));

  // Step 9. Return true.
  return true;
}

// https://html.spec.whatwg.org/#dom-sanitizer-allowprocessinginstruction
bool Sanitizer::AllowProcessingInstruction(
    const StringOrSanitizerProcessingInstruction& aPI) {
  // Step 1. Let configuration be this's configuration.
  // Step 2. Assert: configuration is valid.
  MaybeMaterializeDefaultConfig();

  // Step 3. Set pi to the result of canonicalizing pi.
  RefPtr<nsAtom> pi = CanonicalizeProcessingInstruction(aPI);

  // Step 4. If configuration["processingInstructions"] exists:
  if (mProcessingInstructions) {
    // Step 4.1. If configuration["processingInstructions"] contains pi, then
    // return false.
    //
    // Step 4.2. Append pi to configuration["processingInstructions"].
    //
    // Step 4.3. Return true.
    return mProcessingInstructions->EnsureInserted(pi);
  }

  // Step 5. Otherwise:
  // Step 5.1. If configuration["removeProcessingInstructions"] contains pi:
  if (mRemoveProcessingInstructions->Contains(pi)) {
    // Step 5.1.1. Remove pi from configuration["removeProcessingInstructions"].
    mRemoveProcessingInstructions->Remove(pi);
    // Step 5.1.2. Return true.
    return true;
  }

  // Step 5.2. Return false.
  return false;
}

// https://html.spec.whatwg.org/#dom-sanitizer-removeprocessinginstruction
bool Sanitizer::RemoveProcessingInstruction(
    const StringOrSanitizerProcessingInstruction& aPI) {
  // Step 1. Let configuration be this's configuration.
  // Step 2. Assert: configuration is valid.
  MaybeMaterializeDefaultConfig();

  // Step 3. Set pi to the result of canonicalizing pi.
  RefPtr<nsAtom> pi = CanonicalizeProcessingInstruction(aPI);

  // Step 4. If configuration["processingInstructions"] exists:
  if (mProcessingInstructions) {
    // Step 4.1. If configuration["processingInstructions"] contains pi:
    if (mProcessingInstructions->Contains(pi)) {
      // Step 4.1.1. Remove pi from configuration["processingInstructions"].
      mProcessingInstructions->Remove(pi);
      // Step 4.1.2. Return true.
      return true;
    }

    // Step 4.2. Return false.
    return false;
  }

  // Step 5. Otherwise:
  // Step 5.1. If configuration["removeProcessingInstructions"] contains pi,
  // then return false. Step 5.2. Append pi to
  // configuration["removeProcessingInstructions"]. Step 5.3. Return true.
  return mRemoveProcessingInstructions->EnsureInserted(pi);
}

// https://wicg.github.io/sanitizer-api/#sanitizer-allow-an-attribute
bool Sanitizer::AllowAttribute(
    const StringOrSanitizerAttributeNamespace& aAttribute) {
  MaybeMaterializeDefaultConfig();

  // Step 1. Set attribute to the result of canonicalize a sanitizer attribute
  // with attribute.
  CanonicalAttribute attribute = CanonicalizeAttribute(aAttribute);

  // Step 2. If configuration["attributes"] exists:
  if (mAttributes) {
    // Step 2.1. Comment: If we have a global allow-list, we need to add
    // attribute.

    MOZ_ASSERT(mDataAttributes.isSome(),
               "mDataAttributes exists iff mAttributes exists");

    // Step 2.2. If configuration["dataAttributes"] is true and attribute is a
    // custom data attribute, then return false.
    if (*mDataAttributes && attribute.IsDataAttribute()) {
      return false;
    }

    // Step 2.3. If configuration["attributes"] contains attribute
    // return false.
    if (mAttributes->Contains(attribute)) {
      return false;
    }

    // Step 2.3. Comment: Fix-up per-element allow and remove lists.

    // Step 2.5. If configuration["elements"] exists:
    if (mElements) {
      // Step 2.5.1. For each element in configuration["elements"]:
      for (auto iter = mElements->Iter(); !iter.Done(); iter.Next()) {
        CanonicalElementAttributes& elemAttributes = iter.Data();

        // Step 2.5.1.1. If element["attributes"] with default « [] » contains
        // attribute:
        if (elemAttributes.mAttributes &&
            elemAttributes.mAttributes->Contains(attribute)) {
          // Step 2.5.1.1.1. Remove attribute from element["attributes"].
          elemAttributes.mAttributes->Remove(attribute);
        }

        // Step 2.5.1.2. Assert: element["removeAttributes"] with default « []
        // » does not contain attribute.
        MOZ_ASSERT_IF(elemAttributes.mRemoveAttributes,
                      !elemAttributes.mRemoveAttributes->Contains(attribute));
      }
    }

    // Step 2.6. Append attribute to configuration["attributes"]
    mAttributes->Insert(std::move(attribute));

    // Step 2.7. Return true.
    return true;
  }

  // Step 3. Otherwise:

  // Step 3.1. Comment: If we have a global remove-list, we need to remove
  // attribute.

  // Step 3.2. If configuration["removeAttributes"] does not contain
  // attribute:
  if (!mRemoveAttributes->Contains(attribute)) {
    // Step 3.2.1. Return false.
    return false;
  }

  // Step 3.3. Remove attribute from configuration["removeAttributes"].
  mRemoveAttributes->Remove(attribute);

  // Step 3.4. Return true.
  return true;
}

// https://wicg.github.io/sanitizer-api/#sanitizer-remove-an-attribute
bool Sanitizer::RemoveAttribute(
    const StringOrSanitizerAttributeNamespace& aAttribute) {
  // Step 1. Assert: configuration is valid.
  MaybeMaterializeDefaultConfig();

  // Step 2. Set attribute to the result of canonicalize a sanitizer attribute
  // with attribute.
  CanonicalAttribute attribute = CanonicalizeAttribute(aAttribute);

  return RemoveAttributeCanonical(std::move(attribute));
}

bool Sanitizer::RemoveAttributeCanonical(CanonicalAttribute&& aAttribute) {
  // Step 3. If configuration["attributes"] exists:
  if (mAttributes) {
    // Step 3.1. Comment: If we have a global allow-list, we need to remove
    // attribute.

    // Step 3.2. Set modified to the result of remove attribute from
    // configuration["attributes"].
    bool modified = mAttributes->EnsureRemoved(aAttribute);

    // Step 3.3. Comment: Fix-up per-element allow and remove lists.

    // Step 3.4. If configuration["elements"] exists:
    if (mElements) {
      // Step 3.4.1. For each element of configuration["elements"]:
      for (auto iter = mElements->Iter(); !iter.Done(); iter.Next()) {
        CanonicalElementAttributes& elemAttributes = iter.Data();
        // Step 3.4.1.1. If element["attributes"] with default « » contains
        // attribute:
        if (elemAttributes.mAttributes &&
            elemAttributes.mAttributes->Contains(aAttribute)) {
          // Step 3.4.1.1.1. Set modified to true.
          modified = true;

          // Step 3.4.1.1.2. Remove attribute from element["attributes"].
          elemAttributes.mAttributes->Remove(aAttribute);
        }

        // Step 3.4.1.2. If element["removeAttributes"] with default « [] »
        // contains attribute:
        if (elemAttributes.mRemoveAttributes &&
            elemAttributes.mRemoveAttributes->Contains(aAttribute)) {
          // Step 3.4.1.2.1. Assert: modified is true.
          MOZ_ASSERT(modified,
                     "Must have removed attribute from mAttributes already");

          // Step 3.4.1.2.2. Remove attribute from
          // element["removeAttributes"].
          elemAttributes.mRemoveAttributes->Remove(aAttribute);
        }
      }
    }

    // Step 3.5. Return modified.
    return modified;
  }

  // Step 4. Otherwise:
  // Step 4.1. Comment: If we have a global remove-list, we need to add
  // attribute.

  // Step 4.2. If configuration["removeAttributes"] contains attribute return
  // false.
  if (mRemoveAttributes->Contains(aAttribute)) {
    return false;
  }

  // Step 4.3. Comment: Fix-up per-element allow and remove lists.
  // Step 4.4. If configuration["elements"] exists:
  if (mElements) {
    // Step 4.4.1. For each element in configuration["elements"]:
    for (auto iter = mElements->Iter(); !iter.Done(); iter.Next()) {
      CanonicalElementAttributes& elemAttributes = iter.Data();
      // Step 4.4.1.1. If element["attributes"] with default « [] » contains
      // attribute:
      if (elemAttributes.mAttributes &&
          elemAttributes.mAttributes->Contains(aAttribute)) {
        // Step 4.4.1.1.1. Remove attribute from element["attributes"].
        elemAttributes.mAttributes->Remove(aAttribute);
      }

      // Step 4.4.1.2. If element["removeAttributes"] with default « [] »
      // contains attribute:
      if (elemAttributes.mRemoveAttributes &&
          elemAttributes.mRemoveAttributes->Contains(aAttribute)) {
        // Step 4.4.1.2.1. Remove attribute from element["removeAttributes"].
        elemAttributes.mRemoveAttributes->Remove(aAttribute);
      }
    }
  }

  // Step 4.5. Append attribute to configuration["removeAttributes"].
  mRemoveAttributes->Insert(std::move(aAttribute));

  // Step 4.6. Return true.
  return true;
}

// https://wicg.github.io/sanitizer-api/#sanitizer-set-comments
bool Sanitizer::SetComments(bool aAllow) {
  // The sanitize algorithm optimized for the default config supports
  // comments both being allowed and disallowed.

  // Step 1. If configuration["comments"] exists and configuration["comments"]
  // equals allow, then return false;
  if (mComments == aAllow) {
    return false;
  }

  // Step 2. Set configuration["comments"] to allow.
  mComments = aAllow;

  // Step 3. Return true.
  return true;
}

// https://wicg.github.io/sanitizer-api/#sanitizer-set-data-attributes
bool Sanitizer::SetDataAttributes(bool aAllow) {
  // Same as above for data-attributes.

  // Step 1. If configuration["attributes"] does not exist, then return false.
  // Note: The default config has "attributes".
  if (!mIsDefaultConfig && !mAttributes) {
    return false;
  }

  MOZ_ASSERT(mDataAttributes.isSome(),
             "mDataAttributes exists iff mAttributes exists (or in the default "
             "config)");

  // Step 2. If configuration["dataAttributes"] equals allow, then return false.
  if (*mDataAttributes == aAllow) {
    return false;
  }

  // Step 3. If allow is true:
  if (!mIsDefaultConfig && aAllow) {
    // Note: The default config does not contain any data-attributes.

    // Step 3.1. Remove any items attr from configuration["attributes"] where
    // attr is a custom data attribute.
    mAttributes->RemoveIf([](const CanonicalAttribute& aAttribute) {
      return aAttribute.IsDataAttribute();
    });

    // Step 3.2. If configuration["elements"] exists:
    if (mElements) {
      // Step 3.2.1. For each element in configuration["elements"]:
      for (auto iter = mElements->Iter(); !iter.Done(); iter.Next()) {
        CanonicalElementAttributes& elemAttributes = iter.Data();
        // Step 3.2.1.1. If element[attributes] exists:
        if (elemAttributes.mAttributes) {
          // Step 3.2.1.1.1. Remove any items attr from element[attributes]
          // where attr is a custom data attribute.
          elemAttributes.mAttributes->RemoveIf(
              [](const CanonicalAttribute& aAttribute) {
                return aAttribute.IsDataAttribute();
              });
        }
      }
    }
  }

  // Step 4. Set configuration["dataAttributes"] to allow.
  mDataAttributes = Some(aAllow);

  // Step 5. Return true.
  return true;
}

// https://wicg.github.io/sanitizer-api/#built-in-safe-baseline-configuration
// The built-in safe baseline configuration
#define FOR_EACH_BASELINE_REMOVE_ELEMENT(ELEMENT) \
  ELEMENT(XHTML, xhtml, base)                     \
  ELEMENT(XHTML, xhtml, embed)                    \
  ELEMENT(XHTML, xhtml, frame)                    \
  ELEMENT(XHTML, xhtml, iframe)                   \
  ELEMENT(XHTML, xhtml, object)                   \
  ELEMENT(XHTML, xhtml, script)                   \
  ELEMENT(SVG, svg, script)                       \
  ELEMENT(SVG, svg, use)

// https://wicg.github.io/sanitizer-api/#sanitizerconfig-remove-unsafe
bool Sanitizer::RemoveUnsafe() {
  // XXX this should be a no-op for the default config now.
  MaybeMaterializeDefaultConfig();

  // Step 1. Assert: The key set of built-in safe baseline configuration equals
  // «[ "removeElements", "removeAttributes" ] ».

  // Step 2. Let result be false.
  bool result = false;

  // Step 3. For each element in built-in safe baseline
  // configuration[removeElements]:
#define ELEMENT(_, NSURI, LOCAL_NAME)                                       \
  /* Step 3.1. Call remove an element element from configuration. */        \
  if (RemoveElementCanonical(CanonicalElement(nsGkAtoms::LOCAL_NAME,        \
                                              nsGkAtoms::nsuri_##NSURI))) { \
    /* Step 3.2. If the call returned true, set result to true. */          \
    result = true;                                                          \
  }

  FOR_EACH_BASELINE_REMOVE_ELEMENT(ELEMENT)

#undef ELEMENT

  // Step 4. For each attribute in built-in safe baseline
  // configuration[removeAttributes]:
  // (This is an empty list)

  // Step 5. For each attribute listed in event handler content attributes:
  // TODO: Consider sorting these.
  nsContentUtils::ForEachEventAttributeName(
      EventNameType_All & ~EventNameType_XUL,
      [self = MOZ_KnownLive(this), &result](nsAtom* aName) {
        // Step 5.1. Call remove an attribute attribute from configuration.
        if (self->RemoveAttributeCanonical(
                CanonicalAttribute(aName, nullptr))) {
          // Step 5.2. If the call returned true, set result to true.
          result = true;
        }
      });

  // Step 6. Return result.
  return result;
}

// https://wicg.github.io/sanitizer-api/#sanitize
void Sanitizer::Sanitize(nsINode* aNode, bool aSafe, ErrorResult& aRv) {
  MOZ_ASSERT(aNode->OwnerDoc()->IsLoadedAsData(),
             "SanitizeChildren relies on the document being inert to be safe");

  // Step 1. Let configuration be the value of sanitizer’s configuration.

  // Step 2. If safe is true, then set configuration to the result of calling
  // remove unsafe on configuration.
  //
  // Optimization: We really don't want to make a copy of the configuration
  // here, so we instead explictly remove the handful elements and
  // attributes that are part of "remove unsafe" in the
  // SanitizeChildren() and SanitizeAttributes() methods.

  // Step 3. Call sanitize core on node, configuration, and with
  // handleJavascriptNavigationUrls set to safe.
  if (mIsDefaultConfig) {
    AssertNoLists();
    SanitizeChildren<true>(aNode, aSafe);
  } else {
    AssertIsValid();
    SanitizeChildren<false>(aNode, aSafe);
  }
}

static RefPtr<nsAtom> ToNamespace(int32_t aNamespaceID) {
  if (aNamespaceID == kNameSpaceID_None) {
    return nullptr;
  }

  RefPtr<nsAtom> atom =
      nsNameSpaceManager::GetInstance()->NameSpaceURIAtom(aNamespaceID);
  return atom;
}

static bool IsUnsafeElement(nsAtom* aLocalName, int32_t aNamespaceID) {
#define ELEMENT(NSID, _, LOCAL_NAME)           \
  if (aNamespaceID == kNameSpaceID_##NSID) {   \
    if (aLocalName == nsGkAtoms::LOCAL_NAME) { \
      return true;                             \
    }                                          \
  }

  FOR_EACH_BASELINE_REMOVE_ELEMENT(ELEMENT)

#undef ELEMENT

  return false;
}

// https://wicg.github.io/sanitizer-api/#sanitize-core
// Step 2.4.9.5. If handleJavascriptNavigationUrls:
static bool RemoveJavascriptNavigationURLAttribute(Element* aElement,
                                                   nsAtom* aLocalName,
                                                   int32_t aNamespaceID) {
  // https://wicg.github.io/sanitizer-api/#contains-a-javascript-url
  auto containsJavascriptURL = [&]() {
    nsAutoString value;
    if (!aElement->GetAttr(aNamespaceID, aLocalName, value)) {
      return false;
    }

    // Step 1. Let url be the result of running the basic URL parser on
    // attribute’s value.
    nsCOMPtr<nsIURI> uri;
    if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), value))) {
      // Step 2. If url is failure, then return false.
      return false;
    }

    // Step 3. Return whether url’s scheme is "javascript".
    return uri->SchemeIs("javascript");
  };

  // Step 1. If «[elementName, attrName]» matches an entry in the built-in
  // navigating URL attributes list, and if attribute contains a javascript:
  // URL, then remove attribute from child.
  if ((aElement->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area) &&
       aLocalName == nsGkAtoms::href && aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input) &&
       aLocalName == nsGkAtoms::formaction &&
       aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsHTMLElement(nsGkAtoms::form) &&
       aLocalName == nsGkAtoms::action && aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsHTMLElement(nsGkAtoms::iframe) &&
       aLocalName == nsGkAtoms::src && aNamespaceID == kNameSpaceID_None) ||
      (aElement->IsSVGElement(nsGkAtoms::a) && aLocalName == nsGkAtoms::href &&
       (aNamespaceID == kNameSpaceID_None ||
        aNamespaceID == kNameSpaceID_XLink))) {
    if (containsJavascriptURL()) {
      return true;
    }
  };

  // Step 2. If child’s namespace is the MathML Namespace and attr’s local
  // name is "href" and attr’s namespace is null or the XLink namespace and
  // attr contains a javascript: URL, then remove attr.
  if (aElement->IsMathMLElement() && aLocalName == nsGkAtoms::href &&
      (aNamespaceID == kNameSpaceID_None ||
       aNamespaceID == kNameSpaceID_XLink)) {
    if (containsJavascriptURL()) {
      return true;
    }
  }

  // Step 3. If the built-in animating URL attributes list contains
  // «[elementName, attrName]» and attr’s value is "href" or "xlink:href",
  // then remove attr.
  if (aLocalName == nsGkAtoms::attributeName &&
      aNamespaceID == kNameSpaceID_None &&
      aElement->IsAnyOfSVGElements(
          nsGkAtoms::animate, nsGkAtoms::animateTransform, nsGkAtoms::set)) {
    nsAutoString value;
    if (!aElement->GetAttr(aNamespaceID, aLocalName, value)) {
      return false;
    }

    return value.EqualsLiteral("href") || StringEndsWith(value, u":href"_ns);
  }

  return false;
}

// https://wicg.github.io/sanitizer-api/#sanitize-core
template <bool IsDefaultConfig>
void Sanitizer::SanitizeChildren(nsINode* aNode, bool aSafe) const {
  // Step 1. For each child in current’s children:
  nsCOMPtr<nsIContent> next = nullptr;
  for (nsCOMPtr<nsIContent> child = aNode->GetFirstChild(); child;
       child = next) {
    next = child->GetNextSibling();

    // Step 1.1. Assert: child implements Text, Comment, Element, or
    // DocumentType.
    // TODO(bug 2044714): PI
    MOZ_ASSERT(child->IsText() || child->IsComment() || child->IsElement() ||
               child->NodeType() == nsINode::DOCUMENT_TYPE_NODE);

    // Step 1.2. If child is a DocumentType or Text node, then continue.
    if (child->NodeType() == nsINode::DOCUMENT_TYPE_NODE || child->IsText()) {
      continue;
    }

    // Step 1.3. If child is a Comment node:
    if (child->IsComment()) {
      // Step 1.3.1. If configuration["comments"] is not true, then remove
      // child.
      if (!mComments) {
        child->Remove();
      }
      // Step 1.3.2. Continue.
      continue;
    }

    // Step 1.4. If child is a ProcessingInstruction node:
    // TODO(bug 2044714): Implement this when the HTML parser can produce PI
    // nodes.

    // Step 1.5. Otherwise:
    MOZ_ASSERT(child->IsElement());

    // Step 1.5.1. Let elementName be a SanitizerElementNamespace with child’s
    // local name and namespace.
    nsAtom* nameAtom = child->NodeInfo()->NameAtom();
    int32_t namespaceID = child->NodeInfo()->NamespaceID();
    // Make sure this is optimized away when using the default config.
    Maybe<CanonicalElement> elementName;
    std::conditional_t<IsDefaultConfig, StaticAtomSet*,
                       CanonicalElementAttributes*>
        elementAttributes = nullptr;
    if constexpr (!IsDefaultConfig) {
      elementName.emplace(nameAtom, ToNamespace(namespaceID));

      // Optimization: Remove unsafe elements before doing anything else.
      // https://wicg.github.io/sanitizer-api/#built-in-safe-baseline-configuration
      //
      // We have to do this _before_ handling the
      // "replaceWithChildrenElements" list, because the "remove an element"
      // call in removeUnsafe() would implicitly remove it from the list.
      //
      // The default config's "elements" allow list does not contain any
      // unsafe elements so we can skip this.
      if (aSafe && IsUnsafeElement(nameAtom, namespaceID)) {
        child->Remove();
        continue;
      }

      // Step 1.5.2. If configuration["replaceWithChildrenElements"] exists
      // and if configuration["replaceWithChildrenElements"] contains
      // elementName:
      if (mReplaceWithChildrenElements &&
          mReplaceWithChildrenElements->Contains(*elementName)) {
        // Note: This follows nsTreeSanitizer by first inserting the
        // child's children in place of the current child and then
        // continueing the sanitization from the first inserted grandchild.
        nsCOMPtr<nsIContent> parent = child->GetParent();
        MOZ_DIAGNOSTIC_ASSERT(parent);
        nsCOMPtr<nsIContent> firstChild = child->GetFirstChild();
        nsCOMPtr<nsIContent> newChild = firstChild;
        for (; newChild; newChild = child->GetFirstChild()) {
          ErrorResult rv;
          parent->InsertBefore(*newChild, child, rv);
          if (rv.Failed()) {
            // TODO: Abort?
            break;
          }
        }

        child->Remove();
        if (firstChild) {
          next = firstChild;
        }
        continue;
      }

      // Step 1.5.3. If configuration["removeElements"] exists and
      // configuration["removeElements"] contains elementName:
      if (mRemoveElements) {
        if (mRemoveElements->Contains(*elementName)) {
          // Step 1.5.3.1. Remove child.
          child->Remove();
          // Step 1.5.3.2.Continue.
          continue;
        }
      }

      // Step 1.5.4. If configuration["elements"] exists and
      // configuration["elements"] does not contain elementName:
      if (mElements) {
        elementAttributes = mElements->Lookup(*elementName).DataPtrOrNull();
        if (!elementAttributes) {
          // Step 1.5.4.1. Remove child.
          child->Remove();
          // Step 1.5.4.2. Continue.
          continue;
        }
      }
    } else {
      // (The default config has no replaceWithChildrenElements or
      // removeElements)

      // Step 1.5.4. If configuration["elements"] exists and
      // configuration["elements"] does not contain elementName:

      bool found = false;
      if (nameAtom->IsStatic()) {
        ElementsWithAttributes* elements = nullptr;
        if (namespaceID == kNameSpaceID_XHTML) {
          elements = sDefaultHTMLElements;
        } else if (namespaceID == kNameSpaceID_MathML) {
          elements = sDefaultMathMLElements;
        } else if (namespaceID == kNameSpaceID_SVG) {
          elements = sDefaultSVGElements;
        }
        if (elements) {
          if (auto lookup = elements->Lookup(nameAtom->AsStatic())) {
            found = true;
            // This is the nullptr for elements without specific allowed
            // attributes.
            elementAttributes = lookup->get();
          }
        }
      }
      if (!found) {
        // Step 1.5.4.1. Remove child.
        child->Remove();
        // Step 1.5.4.2. Continue.
        continue;
      }

      MOZ_ASSERT(!IsUnsafeElement(nameAtom, namespaceID),
                 "The default config has no unsafe elements");
    }

    // Step 1.5.5. If elementName equals «[ "name" → "template", "namespace" →
    // HTML namespace ]», then call sanitize core on child’s template contents
    // with configuration and handleJavascriptNavigationUrls.
    if (auto* templateEl = HTMLTemplateElement::FromNode(child)) {
      RefPtr<DocumentFragment> frag = templateEl->Content();
      SanitizeChildren<IsDefaultConfig>(frag, aSafe);
    }

    // Step 1.5.6. If child is a shadow host, then call sanitize core on child’s
    // shadow root with configuration and handleJavascriptNavigationUrls.
    if (RefPtr<ShadowRoot> shadow = child->GetShadowRoot();
        shadow && !shadow->IsUAWidget()) {
      SanitizeChildren<IsDefaultConfig>(shadow, aSafe);
    }

    // Step 1.6.7. If child’s is value is not null:
    if (CustomElementData* data = child->AsElement()->GetCustomElementData();
        data && data->GetIs(child->AsElement())) [[unlikely]] {
      // Step 1.6.7.1. Let isAttrName be «[ "name" → "is", "namespace" → null
      // ]».
      // Step 1.6.7.2. If is attribute allowed for isAttrName given
      // configuration, and elementName is blocked:
      if (IsDefaultConfig ||
          !IsAttributeAllowed(elementAttributes, nsGkAtoms::is,
                              kNameSpaceID_None, aSafe)) {
        // Step 1.6.7.2.1. Step Set child’s custom element state to "undefined".
        // Step 1.6.7.2.2. Set child’s custom element definition to null.
        // Step 1.6.7.2.3. Set child’s is value to null.
        child->AsElement()->ClearCustomElementData();
      }
    }

    // Step 1.6.7. For each attribute in child’s attribute list:
    int32_t attrCount = int32_t(child->AsElement()->GetAttrCount());
    for (int32_t i = attrCount - 1; i >= 0; --i) {
      // Step 1.6.7.1.  Let attrName be a SanitizerAttributeNamespace with
      // attribute’s local name and namespace.
      const nsAttrName* attr = child->AsElement()->GetAttrNameAt(i);
      RefPtr<nsAtom> attrLocalName = attr->LocalName();
      int32_t attrNs = attr->NamespaceID();

      // Step 1.6.7.2. If is attribute allowed for attrName given configuration,
      // and elementName is blocked, then remove attribute.
      bool remove =
          !IsAttributeAllowed(elementAttributes, attrLocalName, attrNs, aSafe);

      // Step 1.6.7.3. If handleJavascriptNavigationUrls:
      if (aSafe && !remove) {
        remove = RemoveJavascriptNavigationURLAttribute(child->AsElement(),
                                                        attrLocalName, attrNs);
      }

      if (remove) {
        child->AsElement()->UnsetAttr(attrNs, attrLocalName,
                                      /* aNotify */ false);

        // XXX Copied from nsTreeSanitizer.
        // In case the attribute removal shuffled the attribute order, start the
        // loop again.
        --attrCount;
        i = attrCount;  // i will be decremented immediately thanks to the for
                        // loop
      }
    }

    // Step 1.5.10. Call sanitize core on child with configuration and
    // handleJavascriptNavigationUrls.
    // TODO: Optimization: Remove recusion similar to nsTreeSanitizer
    SanitizeChildren<IsDefaultConfig>(child, aSafe);
  }
}

static inline bool IsDataAttribute(nsAtom* aName, int32_t aNamespaceID) {
  return StringBeginsWith(nsDependentAtomString(aName), u"data-"_ns) &&
         aNamespaceID == kNameSpaceID_None;
}

// IsAttributeAllowed specialized for the default config.
bool Sanitizer::IsAttributeAllowed(StaticAtomSet* aElementAttributes,
                                   nsAtom* aAttrLocalName, int32_t aAttrNs,
                                   bool) const {
  MOZ_ASSERT(mIsDefaultConfig);

  // Step 1. Let elementWithLocalAttributes be an empty ordered map.
  // Step 2. If configuration["elements"] exists and configuration["elements"]
  // contains elementName:
  //  ...
  // NOTE: This was lookup was already done outside of this function.

  // Step 3. If elementWithLocalAttributes["removeAttributes"] with default « »
  // contains attrName:
  // NOTE: No local removeAttributes in the default config.

  // Step 4. If configuration["attributes"] exists:
  // NOTE: Always true in the default config.

  // Step 4.1. Let the boolean globallyAllowed be whether
  // configuration["attributes"] contains attrName.
  // NOTE: All attributes allowed by the default config are in the "null"
  // namespace.
  bool globallyAllowed = aAttrNs == kNameSpaceID_None &&
                         sDefaultAttributes->Contains(aAttrLocalName);

  // Step 4.2. Let the boolean locallyAllowed be whether
  // elementWithLocalAttributes["attributes"] with default « » contains
  // attrName.
  bool locallyAllowed = aAttrNs == kNameSpaceID_None && aElementAttributes &&
                        aElementAttributes->Contains(aAttrLocalName);

  // Step 4.3. Let the boolean isDataAttributeAllowed be whether both, "data-"
  // is a code unit prefix of attrName[name] and attrName[namespace] is null,
  // and configuration["dataAttributes"] is true.
  bool isDataAttributeAllowed =
      *mDataAttributes && IsDataAttribute(aAttrLocalName, aAttrNs);

  // Step 4.4. If neither globallyAllowed nor locallyAllowed nor
  // isDataAttributeAllowed, return blocked.
  if (!globallyAllowed && !locallyAllowed && !isDataAttributeAllowed) {
    return false;
  }

  // 5. Otherwise:
  // ...

  // The default config attribute allow lists don't contain event
  // handler attributes.
  MOZ_ASSERT(!nsContentUtils::IsEventAttributeName(
      aAttrLocalName, EventNameType_All & ~EventNameType_XUL));

  // Step 6. Return allowed.
  return true;
}

bool Sanitizer::IsAttributeAllowed(
    CanonicalElementAttributes* aElementAttributes, nsAtom* aAttrLocalName,
    int32_t aAttrNs, bool aSafe) const {
  MOZ_ASSERT(!mIsDefaultConfig);

  // Step 1. Let elementWithLocalAttributes be an empty ordered map.
  // Step 2. If configuration["elements"] exists and configuration["elements"]
  // contains elementName:
  //  ...
  // NOTE: This was lookup was already done outside of this function.

  // Optimization: Remove unsafe event handler content attributes here, because
  // we didn't clone the config and called removeUnsafe on it before.
  // https://wicg.github.io/sanitizer-api/#sanitizerconfig-remove-unsafe
  if (aSafe && aAttrNs == kNameSpaceID_None &&
      nsContentUtils::IsEventAttributeName(
          aAttrLocalName, EventNameType_All & ~EventNameType_XUL)) {
    return false;
  }

  CanonicalAttribute attrName(aAttrLocalName, ToNamespace(aAttrNs));
  // Step 3. If elementWithLocalAttributes["removeAttributes"] with default « »
  // contains attrName:
  if (aElementAttributes && aElementAttributes->mRemoveAttributes &&
      aElementAttributes->mRemoveAttributes->Contains(attrName)) {
    // Step 3.1. Return blocked.
    return false;
  }

  // Step 4. If configuration["attributes"] exists:
  if (mAttributes) {
    // Step 4.1. Let the boolean globallyAllowed be whether
    // configuration["attributes"] contains attrName.
    bool globallyAllowed = mAttributes->Contains(attrName);

    // Step 4.2. Let the boolean locallyAllowed be whether
    // elementWithLocalAttributes["attributes"] with default « » contains
    // attrName.
    bool locallyAllowed = aElementAttributes &&
                          aElementAttributes->mAttributes &&
                          aElementAttributes->mAttributes->Contains(attrName);

    // Step 4.3. Let the boolean isDataAttributeAllowed be whether both, "data-"
    // is a code unit prefix of attrName[name] and attrName[namespace] is null,
    // and configuration["dataAttributes"] is true.
    bool isDataAttributeAllowed =
        *mDataAttributes && IsDataAttribute(aAttrLocalName, aAttrNs);

    // Step 4.4. If neither globallyAllowed nor locallyAllowed nor
    // isDataAttributeAllowed, return blocked.
    if (!globallyAllowed && !locallyAllowed && !isDataAttributeAllowed) {
      return false;
    }
  }
  // 5. Otherwise:
  else {
    // Step 5.1. If elementWithLocalAttributes["attributes"] exists and
    // elementWithLocalAttributes["attributes"] does not contain attrName:
    if (aElementAttributes && aElementAttributes->mAttributes &&
        !aElementAttributes->mAttributes->Contains(attrName)) {
      // Step 5.1.1. Return blocked.
      return false;
    }

    // Step 5.2. If configuration["removeAttributes"] contains attrName:
    if (mRemoveAttributes->Contains(attrName)) {
      // Step 5.2.1. Return blocked.
      return false;
    }
  }

  // Step 6. Return allowed.
  return true;
}

}  // namespace mozilla::dom
