/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "txMozillaXMLOutput.h"

#include "mozilla/dom/Document.h"
#include "nsIDocShell.h"
#include "nsIScriptElement.h"
#include "nsCharsetSource.h"
#include "nsIRefreshURI.h"
#include "nsPIDOMWindow.h"
#include "nsIContent.h"
#include "nsContentCID.h"
#include "nsUnicharUtils.h"
#include "nsGkAtoms.h"
#include "txLog.h"
#include "nsNameSpaceManager.h"
#include "txStringUtils.h"
#include "txURIUtils.h"
#include "nsIDocumentTransformer.h"
#include "mozilla/StyleSheetInlines.h"
#include "mozilla/css/Loader.h"
#include "mozilla/dom/DocumentType.h"
#include "mozilla/dom/DocumentFragment.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/ScriptLoader.h"
#include "mozilla/Encoding.h"
#include "nsContentUtils.h"
#include "nsDocElementCreatedNotificationRunner.h"
#include "txXMLUtils.h"
#include "nsContentSink.h"
#include "nsINode.h"
#include "nsContentCreatorFunctions.h"
#include "nsError.h"
#include "nsStringFlags.h"
#include "nsStyleUtil.h"
#include "nsIFrame.h"
#include <algorithm>
#include "nsTextNode.h"
#include "nsDocShell.h"
#include "mozilla/dom/Comment.h"
#include "mozilla/dom/ProcessingInstruction.h"

using namespace mozilla;
using namespace mozilla::dom;

#define TX_ENSURE_CURRENTNODE                            \
  NS_ASSERTION(mCurrentNode, "mCurrentNode is nullptr"); \
  if (!mCurrentNode) return NS_ERROR_UNEXPECTED

txMozillaXMLOutput::txMozillaXMLOutput(Document* aSourceDocument,
                                       txOutputFormat* aFormat,
                                       nsITransformObserver* aObserver)
    : mTreeDepth(0),
      mBadChildLevel(0),
      mTableState(NORMAL),
      mCreatingNewDocument(true),
      mOpenedElementIsHTML(false),
      mRootContentCreated(false),
      mNoFixup(false) {
  MOZ_COUNT_CTOR(txMozillaXMLOutput);
  if (aObserver) {
    mNotifier = new txTransformNotifier(aSourceDocument);
    if (mNotifier) {
      mNotifier->Init(aObserver);
    }
  }

  mOutputFormat.merge(*aFormat);
  mOutputFormat.setFromDefaults();
}

txMozillaXMLOutput::txMozillaXMLOutput(txOutputFormat* aFormat,
                                       DocumentFragment* aFragment,
                                       bool aNoFixup)
    : mTreeDepth(0),
      mBadChildLevel(0),
      mTableState(NORMAL),
      mCreatingNewDocument(false),
      mOpenedElementIsHTML(false),
      mRootContentCreated(false),
      mNoFixup(aNoFixup) {
  MOZ_COUNT_CTOR(txMozillaXMLOutput);
  mOutputFormat.merge(*aFormat);
  mOutputFormat.setFromDefaults();

  mCurrentNode = aFragment;
  mDocument = mCurrentNode->OwnerDoc();
  mNodeInfoManager = mDocument->NodeInfoManager();
}

txMozillaXMLOutput::~txMozillaXMLOutput() {
  MOZ_COUNT_DTOR(txMozillaXMLOutput);
}

nsresult txMozillaXMLOutput::attribute(nsAtom* aPrefix, nsAtom* aLocalName,
                                       nsAtom* aLowercaseLocalName,
                                       const int32_t aNsID,
                                       const nsString& aValue) {
  RefPtr<nsAtom> owner;
  if (mOpenedElementIsHTML && aNsID == kNameSpaceID_None) {
    if (aLowercaseLocalName) {
      aLocalName = aLowercaseLocalName;
    } else {
      owner = TX_ToLowerCaseAtom(aLocalName);
      NS_ENSURE_TRUE(owner, NS_ERROR_OUT_OF_MEMORY);

      aLocalName = owner;
    }
  }

  return attributeInternal(aPrefix, aLocalName, aNsID, aValue);
}

nsresult txMozillaXMLOutput::attribute(nsAtom* aPrefix,
                                       const nsAString& aLocalName,
                                       const int32_t aNsID,
                                       const nsString& aValue) {
  RefPtr<nsAtom> lname;

  if (mOpenedElementIsHTML && aNsID == kNameSpaceID_None) {
    nsAutoString lnameStr;
    nsContentUtils::ASCIIToLower(aLocalName, lnameStr);
    lname = NS_Atomize(lnameStr);
  } else {
    lname = NS_Atomize(aLocalName);
  }

  NS_ENSURE_TRUE(lname, NS_ERROR_OUT_OF_MEMORY);

  // Check that it's a valid name
  if (!nsContentUtils::IsValidNodeName(lname, aPrefix, aNsID)) {
    // Try without prefix
    aPrefix = nullptr;
    if (!nsContentUtils::IsValidNodeName(lname, aPrefix, aNsID)) {
      // Don't return error here since the callers don't deal
      return NS_OK;
    }
  }

  return attributeInternal(aPrefix, lname, aNsID, aValue);
}

nsresult txMozillaXMLOutput::attributeInternal(nsAtom* aPrefix,
                                               nsAtom* aLocalName,
                                               int32_t aNsID,
                                               const nsString& aValue) {
  if (!mOpenedElement) {
    // XXX Signal this? (can't add attributes after element closed)
    return NS_OK;
  }

  NS_ASSERTION(!mBadChildLevel, "mBadChildLevel set when element is opened");

  return mOpenedElement->SetAttr(aNsID, aLocalName, aPrefix, aValue, false);
}

nsresult txMozillaXMLOutput::characters(const nsAString& aData, bool aDOE) {
  nsresult rv = closePrevious(false);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!mBadChildLevel) {
    mText.Append(aData);
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::comment(const nsString& aData) {
  nsresult rv = closePrevious(true);
  NS_ENSURE_SUCCESS(rv, rv);

  if (mBadChildLevel) {
    return NS_OK;
  }

  TX_ENSURE_CURRENTNODE;

  RefPtr<Comment> comment = new (mNodeInfoManager) Comment(mNodeInfoManager);

  rv = comment->SetText(aData, false);
  NS_ENSURE_SUCCESS(rv, rv);

  ErrorResult error;
  mCurrentNode->AppendChildTo(comment, true, error);
  return error.StealNSResult();
}

nsresult txMozillaXMLOutput::endDocument(nsresult aResult) {
  TX_ENSURE_CURRENTNODE;

  if (NS_FAILED(aResult)) {
    if (mNotifier) {
      mNotifier->OnTransformEnd(aResult);
    }

    return NS_OK;
  }

  nsresult rv = closePrevious(true);
  if (NS_FAILED(rv)) {
    if (mNotifier) {
      mNotifier->OnTransformEnd(rv);
    }

    return rv;
  }

  if (mCreatingNewDocument) {
    // This should really be handled by Document::EndLoad
    MOZ_ASSERT(mDocument->GetReadyStateEnum() == Document::READYSTATE_LOADING,
               "Bad readyState");
    mDocument->SetReadyStateInternal(Document::READYSTATE_INTERACTIVE);
    if (ScriptLoader* loader = mDocument->ScriptLoader()) {
      loader->ParsingComplete(false);
    }
  }

  if (mNotifier) {
    mNotifier->OnTransformEnd();
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::endElement() {
  TX_ENSURE_CURRENTNODE;

  if (mBadChildLevel) {
    --mBadChildLevel;
    MOZ_LOG(txLog::xslt, LogLevel::Debug,
            ("endElement, mBadChildLevel = %d\n", mBadChildLevel));
    return NS_OK;
  }

  --mTreeDepth;

  nsresult rv = closePrevious(true);
  NS_ENSURE_SUCCESS(rv, rv);

  NS_ASSERTION(mCurrentNode->IsElement(), "borked mCurrentNode");
  NS_ENSURE_TRUE(mCurrentNode->IsElement(), NS_ERROR_UNEXPECTED);

  Element* element = mCurrentNode->AsElement();

  // Handle html-elements
  if (!mNoFixup) {
    if (element->IsHTMLElement()) {
      endHTMLElement(element);
    }

    // Handle elements that are different when parser-created
    if (nsIContent::RequiresDoneCreatingElement(
            element->NodeInfo()->NamespaceID(),
            element->NodeInfo()->NameAtom())) {
      element->DoneCreatingElement();
    } else if (element->IsSVGElement(nsGkAtoms::script) ||
               element->IsHTMLElement(nsGkAtoms::script)) {
      nsCOMPtr<nsIScriptElement> sele = do_QueryInterface(element);
      if (sele) {
        bool block = sele->AttemptToExecute();
        // If the act of insertion evaluated the script, we're fine.
        // Else, add this script element to the array of loading scripts.
        if (block) {
          mNotifier->AddScriptElement(sele);
        }
      } else {
        MOZ_ASSERT(nsNameSpaceManager::GetInstance()->mSVGDisabled,
                   "Script elements need to implement nsIScriptElement and SVG "
                   "wasn't disabled.");
      }
    } else if (nsIContent::RequiresDoneAddingChildren(
                   element->NodeInfo()->NamespaceID(),
                   element->NodeInfo()->NameAtom())) {
      element->DoneAddingChildren(true);
    }
  }

  if (mCreatingNewDocument) {
    // Handle all sorts of stylesheets
    if (auto* linkStyle = LinkStyle::FromNode(*mCurrentNode)) {
      linkStyle->SetEnableUpdates(true);
      auto updateOrError = linkStyle->UpdateStyleSheet(mNotifier);
      if (mNotifier && updateOrError.isOk() &&
          updateOrError.unwrap().ShouldBlock()) {
        mNotifier->AddPendingStylesheet();
      }
    }
  }

  // Add the element to the tree if it wasn't added before and take one step
  // up the tree
  MOZ_ASSERT(!mCurrentNodeStack.IsEmpty(), "empty stack");
  nsCOMPtr<nsINode> parent;
  if (!mCurrentNodeStack.IsEmpty()) {
    parent = mCurrentNodeStack.PopLastElement();
  }

  if (mCurrentNode == mNonAddedNode) {
    if (parent == mDocument) {
      NS_ASSERTION(!mRootContentCreated,
                   "Parent to add to shouldn't be a document if we "
                   "have a root content");
      mRootContentCreated = true;
    }

    // Check to make sure that script hasn't inserted the node somewhere
    // else in the tree
    if (!mCurrentNode->GetParentNode()) {
      parent->AppendChildTo(mNonAddedNode, true, IgnoreErrors());
    }
    mNonAddedNode = nullptr;
  }

  mCurrentNode = parent;

  mTableState =
      static_cast<TableState>(NS_PTR_TO_INT32(mTableStateStack.pop()));

  return NS_OK;
}

void txMozillaXMLOutput::getOutputDocument(Document** aDocument) {
  NS_IF_ADDREF(*aDocument = mDocument);
}

nsresult txMozillaXMLOutput::processingInstruction(const nsString& aTarget,
                                                   const nsString& aData) {
  nsresult rv = closePrevious(true);
  NS_ENSURE_SUCCESS(rv, rv);

  if (mOutputFormat.mMethod == eHTMLOutput) return NS_OK;

  TX_ENSURE_CURRENTNODE;

  rv = nsContentUtils::CheckQName(aTarget, false);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIContent> pi =
      NS_NewXMLProcessingInstruction(mNodeInfoManager, aTarget, aData);

  LinkStyle* linkStyle = nullptr;
  if (mCreatingNewDocument) {
    linkStyle = LinkStyle::FromNode(*pi);
    if (linkStyle) {
      linkStyle->SetEnableUpdates(false);
    }
  }

  ErrorResult error;
  mCurrentNode->AppendChildTo(pi, true, error);
  if (error.Failed()) {
    return error.StealNSResult();
  }

  if (linkStyle) {
    linkStyle->SetEnableUpdates(true);
    auto updateOrError = linkStyle->UpdateStyleSheet(mNotifier);
    if (mNotifier && updateOrError.isOk() &&
        updateOrError.unwrap().ShouldBlock()) {
      mNotifier->AddPendingStylesheet();
    }
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::startDocument() {
  if (mNotifier) {
    mNotifier->OnTransformStart();
  }

  if (mCreatingNewDocument) {
    ScriptLoader* loader = mDocument->ScriptLoader();
    if (loader) {
      loader->BeginDeferringScripts();
    }
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::startElement(nsAtom* aPrefix, nsAtom* aLocalName,
                                          nsAtom* aLowercaseLocalName,
                                          const int32_t aNsID) {
  MOZ_ASSERT(aNsID != kNameSpaceID_None || !aPrefix,
             "Can't have prefix without namespace");

  if (mOutputFormat.mMethod == eHTMLOutput && aNsID == kNameSpaceID_None) {
    RefPtr<nsAtom> owner;
    if (!aLowercaseLocalName) {
      owner = TX_ToLowerCaseAtom(aLocalName);
      NS_ENSURE_TRUE(owner, NS_ERROR_OUT_OF_MEMORY);

      aLowercaseLocalName = owner;
    }
    return startElementInternal(nullptr, aLowercaseLocalName,
                                kNameSpaceID_XHTML);
  }

  return startElementInternal(aPrefix, aLocalName, aNsID);
}

nsresult txMozillaXMLOutput::startElement(nsAtom* aPrefix,
                                          const nsAString& aLocalName,
                                          const int32_t aNsID) {
  int32_t nsId = aNsID;
  RefPtr<nsAtom> lname;

  if (mOutputFormat.mMethod == eHTMLOutput && aNsID == kNameSpaceID_None) {
    nsId = kNameSpaceID_XHTML;

    nsAutoString lnameStr;
    nsContentUtils::ASCIIToLower(aLocalName, lnameStr);
    lname = NS_Atomize(lnameStr);
  } else {
    lname = NS_Atomize(aLocalName);
  }

  // No biggie if we lose the prefix due to OOM
  NS_ENSURE_TRUE(lname, NS_ERROR_OUT_OF_MEMORY);

  // Check that it's a valid name
  if (!nsContentUtils::IsValidNodeName(lname, aPrefix, nsId)) {
    // Try without prefix
    aPrefix = nullptr;
    if (!nsContentUtils::IsValidNodeName(lname, aPrefix, nsId)) {
      return NS_ERROR_XSLT_BAD_NODE_NAME;
    }
  }

  return startElementInternal(aPrefix, lname, nsId);
}

nsresult txMozillaXMLOutput::startElementInternal(nsAtom* aPrefix,
                                                  nsAtom* aLocalName,
                                                  int32_t aNsID) {
  TX_ENSURE_CURRENTNODE;

  if (mBadChildLevel) {
    ++mBadChildLevel;
    MOZ_LOG(txLog::xslt, LogLevel::Debug,
            ("startElement, mBadChildLevel = %d\n", mBadChildLevel));
    return NS_OK;
  }

  nsresult rv = closePrevious(true);
  NS_ENSURE_SUCCESS(rv, rv);

  // Push and init state
  if (mTreeDepth == MAX_REFLOW_DEPTH) {
    // eCloseElement couldn't add the parent so we fail as well or we've
    // reached the limit of the depth of the tree that we allow.
    ++mBadChildLevel;
    MOZ_LOG(txLog::xslt, LogLevel::Debug,
            ("startElement, mBadChildLevel = %d\n", mBadChildLevel));
    return NS_OK;
  }

  ++mTreeDepth;

  mTableStateStack.push(NS_INT32_TO_PTR(mTableState));

  mCurrentNodeStack.AppendElement(mCurrentNode);

  mTableState = NORMAL;
  mOpenedElementIsHTML = false;

  // Create the element
  RefPtr<NodeInfo> ni = mNodeInfoManager->GetNodeInfo(
      aLocalName, aPrefix, aNsID, nsINode::ELEMENT_NODE);

  NS_NewElement(getter_AddRefs(mOpenedElement), ni.forget(),
                mCreatingNewDocument ? FROM_PARSER_XSLT : FROM_PARSER_FRAGMENT);

  // Set up the element and adjust state
  if (!mNoFixup) {
    if (aNsID == kNameSpaceID_XHTML) {
      mOpenedElementIsHTML = (mOutputFormat.mMethod == eHTMLOutput);
      rv = startHTMLElement(mOpenedElement, mOpenedElementIsHTML);
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  if (mCreatingNewDocument) {
    // Handle all sorts of stylesheets
    if (auto* linkStyle = LinkStyle::FromNodeOrNull(mOpenedElement)) {
      linkStyle->SetEnableUpdates(false);
    }
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::closePrevious(bool aFlushText) {
  TX_ENSURE_CURRENTNODE;

  nsresult rv;
  if (mOpenedElement) {
    bool currentIsDoc = mCurrentNode == mDocument;
    if (currentIsDoc && mRootContentCreated) {
      // We already have a document element, but the XSLT spec allows this.
      // As a workaround, create a wrapper object and use that as the
      // document element.

      rv = createTxWrapper();
      NS_ENSURE_SUCCESS(rv, rv);
    }

    ErrorResult error;
    mCurrentNode->AppendChildTo(mOpenedElement, true, error);
    if (error.Failed()) {
      return error.StealNSResult();
    }

    if (currentIsDoc) {
      mRootContentCreated = true;
      nsContentUtils::AddScriptRunner(
          new nsDocElementCreatedNotificationRunner(mDocument));
    }

    mCurrentNode = mOpenedElement;
    mOpenedElement = nullptr;
  } else if (aFlushText && !mText.IsEmpty()) {
    // Text can't appear in the root of a document
    if (mDocument == mCurrentNode) {
      if (XMLUtils::isWhitespace(mText)) {
        mText.Truncate();

        return NS_OK;
      }

      rv = createTxWrapper();
      NS_ENSURE_SUCCESS(rv, rv);
    }
    RefPtr<nsTextNode> text =
        new (mNodeInfoManager) nsTextNode(mNodeInfoManager);

    rv = text->SetText(mText, false);
    NS_ENSURE_SUCCESS(rv, rv);

    ErrorResult error;
    mCurrentNode->AppendChildTo(text, true, error);
    if (error.Failed()) {
      return error.StealNSResult();
    }

    mText.Truncate();
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::createTxWrapper() {
  NS_ASSERTION(mDocument == mCurrentNode,
               "creating wrapper when document isn't parent");

  int32_t namespaceID;
  nsresult rv = nsNameSpaceManager::GetInstance()->RegisterNameSpace(
      nsLiteralString(kTXNameSpaceURI), namespaceID);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<Element> wrapper =
      mDocument->CreateElem(nsDependentAtomString(nsGkAtoms::result),
                            nsGkAtoms::transformiix, namespaceID);

#ifdef DEBUG
  // Keep track of the location of the current documentElement, if there is
  // one, so we can verify later
  uint32_t j = 0, rootLocation = 0;
#endif
  for (nsCOMPtr<nsIContent> childContent = mDocument->GetFirstChild();
       childContent; childContent = childContent->GetNextSibling()) {
#ifdef DEBUG
    if (childContent->IsElement()) {
      rootLocation = j;
    }
#endif

    if (childContent->NodeInfo()->NameAtom() ==
        nsGkAtoms::documentTypeNodeName) {
#ifdef DEBUG
      // The new documentElement should go after the document type.
      // This is needed for cases when there is no existing
      // documentElement in the document.
      rootLocation = std::max(rootLocation, j + 1);
      ++j;
#endif
    } else {
      mDocument->RemoveChildNode(childContent, true);

      ErrorResult error;
      wrapper->AppendChildTo(childContent, true, error);
      if (error.Failed()) {
        return error.StealNSResult();
      }
      break;
    }
  }

  mCurrentNodeStack.AppendElement(wrapper);
  mCurrentNode = wrapper;
  mRootContentCreated = true;
  NS_ASSERTION(rootLocation == mDocument->GetChildCount(),
               "Incorrect root location");
  ErrorResult error;
  mDocument->AppendChildTo(wrapper, true, error);
  return error.StealNSResult();
}

nsresult txMozillaXMLOutput::startHTMLElement(nsIContent* aElement,
                                              bool aIsHTML) {
  nsresult rv = NS_OK;

  if ((!aElement->IsHTMLElement(nsGkAtoms::tr) || !aIsHTML) &&
      NS_PTR_TO_INT32(mTableStateStack.peek()) == ADDED_TBODY) {
    MOZ_ASSERT(!mCurrentNodeStack.IsEmpty(), "empty stack");
    if (mCurrentNodeStack.IsEmpty()) {
      mCurrentNode = nullptr;
    } else {
      mCurrentNode = mCurrentNodeStack.PopLastElement();
    }
    mTableStateStack.pop();
  }

  if (aElement->IsHTMLElement(nsGkAtoms::table) && aIsHTML) {
    mTableState = TABLE;
  } else if (aElement->IsHTMLElement(nsGkAtoms::tr) && aIsHTML &&
             NS_PTR_TO_INT32(mTableStateStack.peek()) == TABLE) {
    RefPtr<Element> tbody;
    rv = createHTMLElement(nsGkAtoms::tbody, getter_AddRefs(tbody));
    NS_ENSURE_SUCCESS(rv, rv);

    ErrorResult error;
    mCurrentNode->AppendChildTo(tbody, true, error);
    if (error.Failed()) {
      return error.StealNSResult();
    }

    mTableStateStack.push(NS_INT32_TO_PTR(ADDED_TBODY));

    mCurrentNodeStack.AppendElement(tbody);
    mCurrentNode = tbody;
  } else if (aElement->IsHTMLElement(nsGkAtoms::head) &&
             mOutputFormat.mMethod == eHTMLOutput) {
    // Insert META tag, according to spec, 16.2, like
    // <META http-equiv="Content-Type" content="text/html; charset=EUC-JP">
    RefPtr<Element> meta;
    rv = createHTMLElement(nsGkAtoms::meta, getter_AddRefs(meta));
    NS_ENSURE_SUCCESS(rv, rv);

    rv = meta->SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
                       u"Content-Type"_ns, false);
    NS_ENSURE_SUCCESS(rv, rv);

    nsAutoString metacontent;
    CopyUTF8toUTF16(mOutputFormat.mMediaType, metacontent);
    metacontent.AppendLiteral("; charset=");
    metacontent.Append(mOutputFormat.mEncoding);
    rv = meta->SetAttr(kNameSpaceID_None, nsGkAtoms::content, metacontent,
                       false);
    NS_ENSURE_SUCCESS(rv, rv);

    // No need to notify since aElement hasn't been inserted yet
    NS_ASSERTION(!aElement->IsInUncomposedDoc(), "should not be in doc");
    ErrorResult error;
    aElement->AppendChildTo(meta, false, error);
    if (error.Failed()) {
      return error.StealNSResult();
    }
  }

  return NS_OK;
}

void txMozillaXMLOutput::endHTMLElement(nsIContent* aElement) {
  if (mTableState == ADDED_TBODY) {
    NS_ASSERTION(aElement->IsHTMLElement(nsGkAtoms::tbody),
                 "Element flagged as added tbody isn't a tbody");
    MOZ_ASSERT(!mCurrentNodeStack.IsEmpty(), "empty stack");
    if (mCurrentNodeStack.IsEmpty()) {
      mCurrentNode = nullptr;
    } else {
      mCurrentNode = mCurrentNodeStack.PopLastElement();
    }
    mTableState =
        static_cast<TableState>(NS_PTR_TO_INT32(mTableStateStack.pop()));
  }
}

nsresult txMozillaXMLOutput::createResultDocument(const nsAString& aName,
                                                  int32_t aNsID,
                                                  Document* aSourceDocument,
                                                  bool aLoadedAsData) {
  nsresult rv;

  // Create the document
  if (mOutputFormat.mMethod == eHTMLOutput) {
    rv = NS_NewHTMLDocument(getter_AddRefs(mDocument), aLoadedAsData);
    NS_ENSURE_SUCCESS(rv, rv);
  } else {
    // We should check the root name/namespace here and create the
    // appropriate document
    rv = NS_NewXMLDocument(getter_AddRefs(mDocument), aLoadedAsData);
    NS_ENSURE_SUCCESS(rv, rv);
  }
  // This should really be handled by Document::BeginLoad
  MOZ_ASSERT(
      mDocument->GetReadyStateEnum() == Document::READYSTATE_UNINITIALIZED,
      "Bad readyState");
  mDocument->SetReadyStateInternal(Document::READYSTATE_LOADING);
  mDocument->SetMayStartLayout(false);
  bool hasHadScriptObject = false;
  nsIScriptGlobalObject* sgo =
      aSourceDocument->GetScriptHandlingObject(hasHadScriptObject);
  NS_ENSURE_STATE(sgo || !hasHadScriptObject);

  mCurrentNode = mDocument;
  mNodeInfoManager = mDocument->NodeInfoManager();

  // Reset and set up the document
  URIUtils::ResetWithSource(mDocument, aSourceDocument);

  // Make sure we set the script handling object after resetting with the
  // source, so that we have the right principal.
  mDocument->SetScriptHandlingObject(sgo);

  mDocument->SetStateObjectFrom(aSourceDocument);

  // Set the charset
  if (!mOutputFormat.mEncoding.IsEmpty()) {
    const Encoding* encoding = Encoding::ForLabel(mOutputFormat.mEncoding);
    if (encoding) {
      mDocument->SetDocumentCharacterSetSource(kCharsetFromOtherComponent);
      mDocument->SetDocumentCharacterSet(WrapNotNull(encoding));
    }
  }

  // Set the mime-type
  if (!mOutputFormat.mMediaType.IsEmpty()) {
    mDocument->SetContentType(mOutputFormat.mMediaType);
  } else if (mOutputFormat.mMethod == eHTMLOutput) {
    mDocument->SetContentType("text/html"_ns);
  } else {
    mDocument->SetContentType("application/xml"_ns);
  }

  if (mOutputFormat.mMethod == eXMLOutput &&
      mOutputFormat.mOmitXMLDeclaration != eTrue) {
    int32_t standalone;
    if (mOutputFormat.mStandalone == eNotSet) {
      standalone = -1;
    } else if (mOutputFormat.mStandalone == eFalse) {
      standalone = 0;
    } else {
      standalone = 1;
    }

    // Could use mOutputFormat.mVersion.get() when we support
    // versions > 1.0.
    static const char16_t kOneDotZero[] = {'1', '.', '0', '\0'};
    mDocument->SetXMLDeclaration(kOneDotZero, mOutputFormat.mEncoding.get(),
                                 standalone);
  }

  // Set up script loader of the result document.
  ScriptLoader* loader = mDocument->ScriptLoader();
  if (mNotifier) {
    loader->AddObserver(mNotifier);
  } else {
    // Don't load scripts, we can't notify the caller when they're loaded.
    loader->SetEnabled(false);
  }

  if (mNotifier) {
    rv = mNotifier->SetOutputDocument(mDocument);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mDocument->InitFeaturePolicy(mDocument->GetChannel());
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Do this after calling OnDocumentCreated to ensure that the
  // PresShell/PresContext has been hooked up and get notified.
  if (mDocument) {
    mDocument->SetCompatibilityMode(eCompatibility_FullStandards);
  }

  // Add a doc-type if requested
  if (!mOutputFormat.mSystemId.IsEmpty()) {
    nsAutoString qName;
    if (mOutputFormat.mMethod == eHTMLOutput) {
      qName.AssignLiteral("html");
    } else {
      qName.Assign(aName);
    }

    nsresult rv = nsContentUtils::CheckQName(qName);
    if (NS_SUCCEEDED(rv)) {
      RefPtr<nsAtom> doctypeName = NS_Atomize(qName);
      if (!doctypeName) {
        return NS_ERROR_OUT_OF_MEMORY;
      }

      // Indicate that there is no internal subset (not just an empty one)
      RefPtr<DocumentType> documentType = NS_NewDOMDocumentType(
          mNodeInfoManager, doctypeName, mOutputFormat.mPublicId,
          mOutputFormat.mSystemId, VoidString());

      ErrorResult error;
      mDocument->AppendChildTo(documentType, true, error);
      if (error.Failed()) {
        return error.StealNSResult();
      }
    }
  }

  return NS_OK;
}

nsresult txMozillaXMLOutput::createHTMLElement(nsAtom* aName,
                                               Element** aResult) {
  NS_ASSERTION(mOutputFormat.mMethod == eHTMLOutput,
               "need to adjust createHTMLElement");

  *aResult = nullptr;

  RefPtr<NodeInfo> ni;
  ni = mNodeInfoManager->GetNodeInfo(aName, nullptr, kNameSpaceID_XHTML,
                                     nsINode::ELEMENT_NODE);

  nsCOMPtr<Element> el;
  nsresult rv = NS_NewHTMLElement(
      getter_AddRefs(el), ni.forget(),
      mCreatingNewDocument ? FROM_PARSER_XSLT : FROM_PARSER_FRAGMENT);
  el.forget(aResult);
  return rv;
}

txTransformNotifier::txTransformNotifier(Document* aSourceDocument)
    : mSourceDocument(aSourceDocument),
      mPendingStylesheetCount(0),
      mInTransform(false) {}

txTransformNotifier::~txTransformNotifier() = default;

NS_IMPL_ISUPPORTS(txTransformNotifier, nsIScriptLoaderObserver,
                  nsICSSLoaderObserver)

NS_IMETHODIMP
txTransformNotifier::ScriptAvailable(nsresult aResult,
                                     nsIScriptElement* aElement,
                                     bool aIsInlineClassicScript, nsIURI* aURI,
                                     uint32_t aLineNo) {
  if (NS_FAILED(aResult) && mScriptElements.RemoveElement(aElement)) {
    SignalTransformEnd();
  }

  return NS_OK;
}

NS_IMETHODIMP
txTransformNotifier::ScriptEvaluated(nsresult aResult,
                                     nsIScriptElement* aElement,
                                     bool aIsInline) {
  if (mScriptElements.RemoveElement(aElement)) {
    SignalTransformEnd();
  }

  return NS_OK;
}

NS_IMETHODIMP
txTransformNotifier::StyleSheetLoaded(StyleSheet* aSheet, bool aWasDeferred,
                                      nsresult aStatus) {
  if (mPendingStylesheetCount == 0) {
    // We weren't waiting on this stylesheet anyway.  This can happen if
    // SignalTransformEnd got called with an error aResult.  See
    // http://bugzilla.mozilla.org/show_bug.cgi?id=215465.
    return NS_OK;
  }

  // We're never waiting for alternate stylesheets
  if (!aWasDeferred) {
    --mPendingStylesheetCount;
    SignalTransformEnd();
  }

  return NS_OK;
}

void txTransformNotifier::Init(nsITransformObserver* aObserver) {
  mObserver = aObserver;
}

void txTransformNotifier::AddScriptElement(nsIScriptElement* aElement) {
  mScriptElements.AppendElement(aElement);
}

void txTransformNotifier::AddPendingStylesheet() { ++mPendingStylesheetCount; }

void txTransformNotifier::OnTransformEnd(nsresult aResult) {
  mInTransform = false;
  SignalTransformEnd(aResult);
}

void txTransformNotifier::OnTransformStart() { mInTransform = true; }

nsresult txTransformNotifier::SetOutputDocument(Document* aDocument) {
  mDocument = aDocument;

  // Notify the contentsink that the document is created
  return mObserver->OnDocumentCreated(mSourceDocument, mDocument);
}

void txTransformNotifier::SignalTransformEnd(nsresult aResult) {
  if (mInTransform ||
      (NS_SUCCEEDED(aResult) &&
       (!mScriptElements.IsEmpty() || mPendingStylesheetCount > 0))) {
    return;
  }

  // mPendingStylesheetCount is nonzero at this point only if aResult is an
  // error.  Set it to 0 so we won't reenter this code when we stop the
  // CSSLoader.
  mPendingStylesheetCount = 0;
  mScriptElements.Clear();

  // Make sure that we don't get deleted while this function is executed and
  // we remove ourselfs from the scriptloader
  nsCOMPtr<nsIScriptLoaderObserver> kungFuDeathGrip(this);

  if (mDocument) {
    mDocument->ScriptLoader()->DeferCheckpointReached();
    mDocument->ScriptLoader()->RemoveObserver(this);
    // XXX Maybe we want to cancel script loads if NS_FAILED(rv)?

    if (NS_FAILED(aResult)) {
      mDocument->CSSLoader()->Stop();
    }
  }

  if (NS_SUCCEEDED(aResult)) {
    mObserver->OnTransformDone(mSourceDocument, aResult, mDocument);
  }
}
