/* 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 "nsHtml5Highlighter.h"
#include "ErrorList.h"
#include "nsDebug.h"
#include "nsHtml5AttributeName.h"
#include "nsHtml5Tokenizer.h"
#include "nsHtml5ViewSourceUtils.h"
#include "nsString.h"
#include "nsThreadUtils.h"

#include "mozilla/Attributes.h"
#include "mozilla/Preferences.h"

using namespace mozilla;

// The old code had a limit of 16 tokens. 1300 is a number picked my measuring
// the size of 16 tokens on cnn.com.
#define NS_HTML5_HIGHLIGHTER_PRE_BREAK_THRESHOLD 1300

char16_t nsHtml5Highlighter::sComment[] = {'c', 'o', 'm', 'm',
                                           'e', 'n', 't', 0};

char16_t nsHtml5Highlighter::sCdata[] = {'c', 'd', 'a', 't', 'a', 0};

char16_t nsHtml5Highlighter::sEntity[] = {'e', 'n', 't', 'i', 't', 'y', 0};

char16_t nsHtml5Highlighter::sEndTag[] = {'e', 'n', 'd', '-', 't', 'a', 'g', 0};

char16_t nsHtml5Highlighter::sStartTag[] = {'s', 't', 'a', 'r', 't',
                                            '-', 't', 'a', 'g', 0};

char16_t nsHtml5Highlighter::sAttributeName[] = {
    'a', 't', 't', 'r', 'i', 'b', 'u', 't', 'e', '-', 'n', 'a', 'm', 'e', 0};

char16_t nsHtml5Highlighter::sAttributeValue[] = {'a', 't', 't', 'r', 'i', 'b',
                                                  'u', 't', 'e', '-', 'v', 'a',
                                                  'l', 'u', 'e', 0};

char16_t nsHtml5Highlighter::sDoctype[] = {'d', 'o', 'c', 't',
                                           'y', 'p', 'e', 0};

char16_t nsHtml5Highlighter::sPi[] = {'p', 'i', 0};

nsHtml5Highlighter::nsHtml5Highlighter(nsAHtml5TreeOpSink* aOpSink)
    : mState(nsHtml5Tokenizer::DATA),
      mCStart(INT32_MAX),
      mPos(0),
      mLineNumber(1),
      mInlinesOpen(0),
      mInCharacters(false),
      mBuffer(nullptr),
      mOpSink(aOpSink),
      mCurrentRun(nullptr),
      mAmpersand(nullptr),
      mSlash(nullptr),
      mHandles(
          MakeUnique<nsIContent*[]>(NS_HTML5_HIGHLIGHTER_HANDLE_ARRAY_LENGTH)),
      mHandlesUsed(0),
      mSeenBase(false) {
  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
}

nsHtml5Highlighter::~nsHtml5Highlighter() {
  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
}

void nsHtml5Highlighter::SetOpSink(nsAHtml5TreeOpSink* aOpSink) {
  mOpSink = aOpSink;
}

void nsHtml5Highlighter::Rewind() {
  mState = 0;
  mCStart = INT32_MAX;
  mPos = 0;
  mLineNumber = 1;
  mInlinesOpen = 0;
  mInCharacters = false;
  mBuffer = nullptr;
  mOpQueue.Clear();
  mCurrentRun = nullptr;
  mAmpersand = nullptr;
  mSlash = nullptr;
  // Pop until we have three elements on the stack:
  // html, body, and pre.
  while (mStack.Length() > 3) {
    Pop();
  }
  mSeenBase = false;
}

void nsHtml5Highlighter::Start(const nsAutoString& aTitle) {
  // Doctype
  opAppendDoctypeToDocument operation(nsGkAtoms::html, u""_ns, u""_ns);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));

  mOpQueue.AppendElement()->Init(mozilla::AsVariant(STANDARDS_MODE));

  // <html> uses NS_NewHTMLSharedElement creator
  nsIContent** root =
      CreateElement(nsGkAtoms::html, nullptr, nullptr, NS_NewHTMLSharedElement);
  opAppendToDocument appendOp(root);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(appendOp));
  mStack.AppendElement(root);

  // <head> uses NS_NewHTMLSharedElement creator
  Push(nsGkAtoms::head, nullptr, NS_NewHTMLSharedElement);

  Push(nsGkAtoms::meta, nsHtml5ViewSourceUtils::NewMetaViewportAttributes(),
       NS_NewHTMLMetaElement);
  Pop();  // meta

  Push(nsGkAtoms::title, nullptr, NS_NewHTMLTitleElement);
  // XUL will add the "Source of: " prefix.
  uint32_t length = aTitle.Length();
  if (length > INT32_MAX) {
    length = INT32_MAX;
  }
  AppendCharacters(aTitle.BeginReading(), 0, (int32_t)length);
  Pop();  // title

  Push(nsGkAtoms::link, nsHtml5ViewSourceUtils::NewLinkAttributes(),
       NS_NewHTMLLinkElement);

  opUpdateStyleSheet updateOp(CurrentNode());
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(updateOp));

  Pop();  // link

  Pop();  // head

  Push(nsGkAtoms::body, nsHtml5ViewSourceUtils::NewBodyAttributes(),
       NS_NewHTMLBodyElement);

  nsHtml5HtmlAttributes* preAttrs = new nsHtml5HtmlAttributes(0);
  nsHtml5String preId = nsHtml5Portability::newStringFromLiteral("line1");
  preAttrs->addAttribute(nsHtml5AttributeName::ATTR_ID, preId, -1);
  Push(nsGkAtoms::pre, preAttrs, NS_NewHTMLPreElement);

  // Don't call StartCharacters here in order to be able to put it in
  // a speculation.

  mOpQueue.AppendElement()->Init(mozilla::AsVariant(opStartLayout()));
}

void nsHtml5Highlighter::UpdateCharsetSource(nsCharsetSource aCharsetSource) {
  opUpdateCharsetSource operation(aCharsetSource);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));
}

int32_t nsHtml5Highlighter::Transition(int32_t aState, bool aReconsume,
                                       int32_t aPos) {
  mPos = aPos;
  switch (mState) {
    case nsHtml5Tokenizer::SCRIPT_DATA:
    case nsHtml5Tokenizer::RAWTEXT:
    case nsHtml5Tokenizer::RCDATA:
    case nsHtml5Tokenizer::DATA:
      // We can transition on < and on &. Either way, we don't yet know the
      // role of the token, so open a span without class.
      if (aState == nsHtml5Tokenizer::CONSUME_CHARACTER_REFERENCE) {
        StartSpan();
        // Start another span for highlighting the ampersand
        StartSpan();
        mAmpersand = CurrentNode();
      } else {
        EndCharactersAndStartMarkupRun();
      }
      break;
    case nsHtml5Tokenizer::TAG_OPEN:
      switch (aState) {
        case nsHtml5Tokenizer::TAG_NAME:
          StartSpan(sStartTag);
          break;
        case nsHtml5Tokenizer::DATA:
          FinishTag();  // DATA
          break;
        case nsHtml5Tokenizer::PROCESSING_INSTRUCTION:
          AddClass(sPi);
          break;
      }
      break;
    case nsHtml5Tokenizer::TAG_NAME:
      switch (aState) {
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME:
          EndSpanOrA();  // nsHtml5Tokenizer::TAG_NAME
          break;
        case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
          EndSpanOrA();  // nsHtml5Tokenizer::TAG_NAME
          StartSpan();   // for highlighting the slash
          mSlash = CurrentNode();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME:
      switch (aState) {
        case nsHtml5Tokenizer::ATTRIBUTE_NAME:
          StartSpan(sAttributeName);
          break;
        case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
          StartSpan();  // for highlighting the slash
          mSlash = CurrentNode();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::ATTRIBUTE_NAME:
      switch (aState) {
        case nsHtml5Tokenizer::AFTER_ATTRIBUTE_NAME:
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_VALUE:
          EndSpanOrA();  // nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME
          break;
        case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
          EndSpanOrA();  // nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME
          StartSpan();   // for highlighting the slash
          mSlash = CurrentNode();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_VALUE:
      switch (aState) {
        case nsHtml5Tokenizer::ATTRIBUTE_VALUE_DOUBLE_QUOTED:
        case nsHtml5Tokenizer::ATTRIBUTE_VALUE_SINGLE_QUOTED:
          FlushCurrent();
          StartA();
          break;
        case nsHtml5Tokenizer::ATTRIBUTE_VALUE_UNQUOTED:
          StartA();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::ATTRIBUTE_VALUE_DOUBLE_QUOTED:
    case nsHtml5Tokenizer::ATTRIBUTE_VALUE_SINGLE_QUOTED:
      switch (aState) {
        case nsHtml5Tokenizer::AFTER_ATTRIBUTE_VALUE_QUOTED:
          EndSpanOrA();
          break;
        case nsHtml5Tokenizer::CONSUME_CHARACTER_REFERENCE:
          StartSpan();
          StartSpan();  // for ampersand itself
          mAmpersand = CurrentNode();
          break;
        default:
          MOZ_ASSERT_UNREACHABLE("Impossible transition.");
          break;
      }
      break;
    case nsHtml5Tokenizer::AFTER_ATTRIBUTE_VALUE_QUOTED:
      switch (aState) {
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME:
          break;
        case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
          StartSpan();  // for highlighting the slash
          mSlash = CurrentNode();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
      EndSpanOrA();  // end the slash highlight
      switch (aState) {
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME:
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::ATTRIBUTE_VALUE_UNQUOTED:
      switch (aState) {
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME:
          EndSpanOrA();
          break;
        case nsHtml5Tokenizer::CONSUME_CHARACTER_REFERENCE:
          StartSpan();
          StartSpan();  // for ampersand itself
          mAmpersand = CurrentNode();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::AFTER_ATTRIBUTE_NAME:
      switch (aState) {
        case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
          StartSpan();  // for highlighting the slash
          mSlash = CurrentNode();
          break;
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_VALUE:
          break;
        case nsHtml5Tokenizer::ATTRIBUTE_NAME:
          StartSpan(sAttributeName);
          break;
        default:
          FinishTag();
          break;
      }
      break;
    // most comment states are omitted, because they don't matter to
    // highlighting
    case nsHtml5Tokenizer::COMMENT_START:
    case nsHtml5Tokenizer::COMMENT_END:
    case nsHtml5Tokenizer::COMMENT_END_BANG:
    case nsHtml5Tokenizer::COMMENT_START_DASH:
    case nsHtml5Tokenizer::BOGUS_COMMENT:
    case nsHtml5Tokenizer::BOGUS_COMMENT_HYPHEN:
    case nsHtml5Tokenizer::COMMENT_LESSTHAN_BANG_DASH_DASH:
      if (aState == nsHtml5Tokenizer::DATA) {
        AddClass(sComment);
        FinishTag();
      }
      break;
    // most cdata states are omitted, because they don't matter to
    // highlighting
    case nsHtml5Tokenizer::CDATA_RSQB_RSQB:
      if (aState == nsHtml5Tokenizer::DATA) {
        AddClass(sCdata);
        FinishTag();
      }
      break;
    case nsHtml5Tokenizer::CONSUME_CHARACTER_REFERENCE:
      EndSpanOrA();  // the span for the ampersand
      switch (aState) {
        case nsHtml5Tokenizer::CONSUME_NCR:
        case nsHtml5Tokenizer::CHARACTER_REFERENCE_HILO_LOOKUP:
          break;
        default:
          // not actually a character reference
          EndSpanOrA();
          break;
      }
      break;
    case nsHtml5Tokenizer::CHARACTER_REFERENCE_HILO_LOOKUP:
      if (aState == nsHtml5Tokenizer::CHARACTER_REFERENCE_TAIL) {
        break;
      }
      // not actually a character reference
      EndSpanOrA();
      break;
    case nsHtml5Tokenizer::CHARACTER_REFERENCE_TAIL:
      if (!aReconsume) {
        FlushCurrent();
      }
      EndSpanOrA();
      break;
    case nsHtml5Tokenizer::DECIMAL_NRC_LOOP:
    case nsHtml5Tokenizer::HEX_NCR_LOOP:
      switch (aState) {
        case nsHtml5Tokenizer::HANDLE_NCR_VALUE:
          AddClass(sEntity);
          FlushCurrent();
          break;
        case nsHtml5Tokenizer::HANDLE_NCR_VALUE_RECONSUME:
          AddClass(sEntity);
          break;
      }
      EndSpanOrA();
      break;
    case nsHtml5Tokenizer::CLOSE_TAG_OPEN:
      switch (aState) {
        case nsHtml5Tokenizer::DATA:
          FinishTag();
          break;
        case nsHtml5Tokenizer::TAG_NAME:
          StartSpan(sEndTag);
          break;
      }
      break;
    case nsHtml5Tokenizer::RAWTEXT_RCDATA_LESS_THAN_SIGN:
      if (aState == nsHtml5Tokenizer::NON_DATA_END_TAG_NAME) {
        FlushCurrent();
        StartSpan();  // don't know if it is "end-tag" yet :-(
        break;
      }
      EndSpanOrA();
      StartCharacters();
      break;
    case nsHtml5Tokenizer::NON_DATA_END_TAG_NAME:
      switch (aState) {
        case nsHtml5Tokenizer::BEFORE_ATTRIBUTE_NAME:
          AddClass(sEndTag);
          EndSpanOrA();
          break;
        case nsHtml5Tokenizer::SELF_CLOSING_START_TAG:
          AddClass(sEndTag);
          EndSpanOrA();
          StartSpan();  // for highlighting the slash
          mSlash = CurrentNode();
          break;
        case nsHtml5Tokenizer::DATA:  // yes, as a result of emitting the token
          AddClass(sEndTag);
          FinishTag();
          break;
        default:
          FinishTag();
          break;
      }
      break;
    case nsHtml5Tokenizer::SCRIPT_DATA_LESS_THAN_SIGN:
    case nsHtml5Tokenizer::SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN:
      if (aState == nsHtml5Tokenizer::NON_DATA_END_TAG_NAME) {
        FlushCurrent();
        StartSpan();  // don't know if it is "end-tag" yet :-(
        break;
      }
      FinishTag();
      break;
    case nsHtml5Tokenizer::SCRIPT_DATA_ESCAPED_DASH_DASH:
    case nsHtml5Tokenizer::SCRIPT_DATA_ESCAPED:
    case nsHtml5Tokenizer::SCRIPT_DATA_ESCAPED_DASH:
      if (aState == nsHtml5Tokenizer::SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN) {
        EndCharactersAndStartMarkupRun();
      }
      break;
    // Lots of double escape states omitted, because they don't highlight.
    // Likewise, only doctype states that can emit the doctype are of
    // interest. Otherwise, the transition out of bogus comment deals.
    case nsHtml5Tokenizer::BEFORE_DOCTYPE_NAME:
    case nsHtml5Tokenizer::DOCTYPE_NAME:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_NAME:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_PUBLIC_KEYWORD:
    case nsHtml5Tokenizer::BEFORE_DOCTYPE_PUBLIC_IDENTIFIER:
    case nsHtml5Tokenizer::DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_PUBLIC_IDENTIFIER:
    case nsHtml5Tokenizer::BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS:
    case nsHtml5Tokenizer::DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_SYSTEM_IDENTIFIER:
    case nsHtml5Tokenizer::BOGUS_DOCTYPE:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_SYSTEM_KEYWORD:
    case nsHtml5Tokenizer::BEFORE_DOCTYPE_SYSTEM_IDENTIFIER:
    case nsHtml5Tokenizer::DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED:
    case nsHtml5Tokenizer::DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED:
      if (aState == nsHtml5Tokenizer::DATA) {
        AddClass(sDoctype);
        FinishTag();
      }
      break;
    case nsHtml5Tokenizer::PROCESSING_INSTRUCTION_QUESTION_MARK:
      if (aState == nsHtml5Tokenizer::DATA) {
        FinishTag();
      }
      break;
    default:
      break;
  }
  mState = aState;
  return aState;
}

[[nodiscard]] bool nsHtml5Highlighter::End() {
  switch (mState) {
    case nsHtml5Tokenizer::COMMENT_END:
    case nsHtml5Tokenizer::COMMENT_END_BANG:
    case nsHtml5Tokenizer::COMMENT_START_DASH:
    case nsHtml5Tokenizer::BOGUS_COMMENT:
    case nsHtml5Tokenizer::BOGUS_COMMENT_HYPHEN:
      AddClass(sComment);
      break;
    case nsHtml5Tokenizer::CDATA_RSQB_RSQB:
      AddClass(sCdata);
      break;
    case nsHtml5Tokenizer::DECIMAL_NRC_LOOP:
    case nsHtml5Tokenizer::HEX_NCR_LOOP:
      // XXX need tokenizer help here
      break;
    case nsHtml5Tokenizer::BEFORE_DOCTYPE_NAME:
    case nsHtml5Tokenizer::DOCTYPE_NAME:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_NAME:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_PUBLIC_KEYWORD:
    case nsHtml5Tokenizer::BEFORE_DOCTYPE_PUBLIC_IDENTIFIER:
    case nsHtml5Tokenizer::DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_PUBLIC_IDENTIFIER:
    case nsHtml5Tokenizer::BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS:
    case nsHtml5Tokenizer::DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_SYSTEM_IDENTIFIER:
    case nsHtml5Tokenizer::BOGUS_DOCTYPE:
    case nsHtml5Tokenizer::AFTER_DOCTYPE_SYSTEM_KEYWORD:
    case nsHtml5Tokenizer::BEFORE_DOCTYPE_SYSTEM_IDENTIFIER:
    case nsHtml5Tokenizer::DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED:
    case nsHtml5Tokenizer::DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED:
      AddClass(sDoctype);
      break;
    default:
      break;
  }
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  treeOp->Init(mozilla::AsVariant(opStreamEnded()));
  return FlushOps().isOk();
}

void nsHtml5Highlighter::SetBuffer(nsHtml5UTF16Buffer* aBuffer) {
  MOZ_ASSERT(!mBuffer, "Old buffer still here!");
  mBuffer = aBuffer;
  mCStart = aBuffer->getStart();
}

void nsHtml5Highlighter::DropBuffer(int32_t aPos) {
  MOZ_ASSERT(mBuffer, "No buffer to drop!");
  mPos = aPos;
  FlushChars();
  mBuffer = nullptr;
}

void nsHtml5Highlighter::StartSpan() {
  FlushChars();
  Push(nsGkAtoms::span, nullptr, NS_NewHTMLSpanElement);
  ++mInlinesOpen;
}

void nsHtml5Highlighter::StartSpan(const char16_t* aClass) {
  StartSpan();
  AddClass(aClass);
}

void nsHtml5Highlighter::EndSpanOrA() {
  FlushChars();
  Pop();
  --mInlinesOpen;
}

void nsHtml5Highlighter::StartCharacters() {
  MOZ_ASSERT(!mInCharacters, "Already in characters!");
  FlushChars();
  Push(nsGkAtoms::span, nullptr, NS_NewHTMLSpanElement);
  mCurrentRun = CurrentNode();
  mInCharacters = true;
}

void nsHtml5Highlighter::EndCharactersAndStartMarkupRun() {
  MOZ_ASSERT(mInCharacters, "Not in characters!");
  FlushChars();
  Pop();
  mInCharacters = false;
  // Now start markup run
  StartSpan();
  mCurrentRun = CurrentNode();
}

void nsHtml5Highlighter::StartA() {
  FlushChars();
  Push(nsGkAtoms::a, nullptr, NS_NewHTMLAnchorElement);
  AddClass(sAttributeValue);
  ++mInlinesOpen;
}

void nsHtml5Highlighter::FinishTag() {
  while (mInlinesOpen > 1) {
    EndSpanOrA();
  }
  FlushCurrent();  // >
  EndSpanOrA();    // DATA
  NS_ASSERTION(!mInlinesOpen, "mInlinesOpen got out of sync!");
  StartCharacters();
}

void nsHtml5Highlighter::FlushChars() {
  if (mCStart < mPos) {
    char16_t* buf = mBuffer->getBuffer();
    int32_t i = mCStart;
    while (i < mPos) {
      char16_t c = buf[i];
      switch (c) {
        case '\r':
          // The input this code sees has been normalized so that there are
          // CR breaks and LF breaks but no CRLF breaks. Overwrite CR with LF
          // to show consistent LF line breaks to layout. It is OK to mutate
          // the input data, because there are no reparses in the View Source
          // case, so we won't need the original data in the buffer anymore.
          buf[i] = '\n';
          [[fallthrough]];
        case '\n': {
          ++i;
          if (mCStart < i) {
            int32_t len = i - mCStart;
            AppendCharacters(buf, mCStart, len);
            mCStart = i;
          }
          ++mLineNumber;
          Push(nsGkAtoms::span, nullptr, NS_NewHTMLSpanElement);
          nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
          NS_ASSERTION(treeOp, "Tree op allocation failed.");
          opAddLineNumberId operation(CurrentNode(), mLineNumber);
          treeOp->Init(mozilla::AsVariant(operation));
          Pop();
          break;
        }
        default:
          ++i;
          break;
      }
    }
    if (mCStart < mPos) {
      int32_t len = mPos - mCStart;
      AppendCharacters(buf, mCStart, len);
      mCStart = mPos;
    }
  }
}

void nsHtml5Highlighter::FlushCurrent() {
  mPos++;
  FlushChars();
}

bool nsHtml5Highlighter::ShouldFlushOps() {
  // Arbitrary threshold that doesn't have an exact justification.
  // The general idea is to flush much, much sooner than reaching
  // the maximum size of `nsTArray`.
  return mOpQueue.Length() > 100000;
}

mozilla::Result<bool, nsresult> nsHtml5Highlighter::FlushOps() {
  bool hasOps = !mOpQueue.IsEmpty();
  if (hasOps) {
    if (!mOpSink->MoveOpsFrom(mOpQueue)) {
      return Err(NS_ERROR_OUT_OF_MEMORY);
    }
  }
  return hasOps;
}

void nsHtml5Highlighter::MaybeLinkifyAttributeValue(nsHtml5AttributeName* aName,
                                                    nsHtml5String aValue) {
  if (!(nsHtml5AttributeName::ATTR_HREF == aName ||
        nsHtml5AttributeName::ATTR_SRC == aName ||
        nsHtml5AttributeName::ATTR_ACTION == aName ||
        nsHtml5AttributeName::ATTR_CITE == aName ||
        nsHtml5AttributeName::ATTR_BACKGROUND == aName ||
        nsHtml5AttributeName::ATTR_LONGDESC == aName ||
        nsHtml5AttributeName::ATTR_XLINK_HREF == aName ||
        nsHtml5AttributeName::ATTR_DEFINITIONURL == aName)) {
    return;
  }
  AddViewSourceHref(aValue);
}

void nsHtml5Highlighter::CompletedNamedCharacterReference() {
  AddClass(sEntity);
}

nsIContent** nsHtml5Highlighter::AllocateContentHandle() {
  if (mHandlesUsed == NS_HTML5_HIGHLIGHTER_HANDLE_ARRAY_LENGTH) {
    mOldHandles.AppendElement(std::move(mHandles));
    mHandles =
        MakeUnique<nsIContent*[]>(NS_HTML5_HIGHLIGHTER_HANDLE_ARRAY_LENGTH);
    mHandlesUsed = 0;
  }
#ifdef DEBUG
  mHandles[mHandlesUsed] = reinterpret_cast<nsIContent*>(uintptr_t(0xC0DEDBAD));
#endif
  return &mHandles[mHandlesUsed++];
}

nsIContent** nsHtml5Highlighter::CreateElement(
    nsAtom* aName, nsHtml5HtmlAttributes* aAttributes,
    nsIContent** aIntendedParent,
    mozilla::dom::HTMLContentCreatorFunction aCreator) {
  MOZ_ASSERT(aName, "Got null name.");
  nsIContent** content = AllocateContentHandle();
  opCreateHTMLElement opeation(content, aName, aAttributes, aCreator,
                               aIntendedParent,
                               mozilla::dom::FROM_PARSER_NETWORK);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(opeation));
  return content;
}

nsIContent** nsHtml5Highlighter::CurrentNode() {
  MOZ_ASSERT(mStack.Length() >= 1, "Must have something on stack.");
  return mStack[mStack.Length() - 1];
}

void nsHtml5Highlighter::Push(
    nsAtom* aName, nsHtml5HtmlAttributes* aAttributes,
    mozilla::dom::HTMLContentCreatorFunction aCreator) {
  MOZ_ASSERT(mStack.Length() >= 1, "Pushing without root.");
  nsIContent** elt = CreateElement(aName, aAttributes, CurrentNode(),
                                   aCreator);  // Don't inline below!
  opAppend operation(elt, CurrentNode(), mozilla::dom::FROM_PARSER_NETWORK);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));
  mStack.AppendElement(elt);
}

void nsHtml5Highlighter::Pop() {
  MOZ_ASSERT(mStack.Length() >= 2, "Popping when stack too short.");
  mStack.RemoveLastElement();
}

void nsHtml5Highlighter::AppendCharacters(const char16_t* aBuffer,
                                          int32_t aStart, int32_t aLength) {
  MOZ_ASSERT(aBuffer, "Null buffer");

  char16_t* bufferCopy = new char16_t[aLength];
  memcpy(bufferCopy, aBuffer + aStart, aLength * sizeof(char16_t));

  opAppendText operation(CurrentNode(), bufferCopy, aLength);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddClass(const char16_t* aClass) {
  opAddClass operation(CurrentNode(), (char16_t*)aClass);
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddViewSourceHref(nsHtml5String aValue) {
  char16_t* bufferCopy = new char16_t[aValue.Length() + 1];
  aValue.CopyToBuffer(bufferCopy);
  bufferCopy[aValue.Length()] = 0;

  opAddViewSourceHref operation(CurrentNode(), bufferCopy, aValue.Length());
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddBase(nsHtml5String aValue) {
  if (mSeenBase) {
    return;
  }
  mSeenBase = true;
  char16_t* bufferCopy = new char16_t[aValue.Length() + 1];
  aValue.CopyToBuffer(bufferCopy);
  bufferCopy[aValue.Length()] = 0;

  opAddViewSourceBase operation(bufferCopy, aValue.Length());
  mOpQueue.AppendElement()->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddErrorToCurrentNode(const char* aMsgId) {
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  opAddErrorType operation(CurrentNode(), (char*)aMsgId);
  treeOp->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddErrorToCurrentRun(const char* aMsgId) {
  MOZ_ASSERT(mCurrentRun, "Adding error to run without one!");
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  opAddErrorType operation(mCurrentRun, (char*)aMsgId);
  treeOp->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddErrorToCurrentRun(const char* aMsgId,
                                              nsAtom* aName) {
  MOZ_ASSERT(mCurrentRun, "Adding error to run without one!");
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  opAddErrorType operation(mCurrentRun, (char*)aMsgId, aName);
  treeOp->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddErrorToCurrentRun(const char* aMsgId, nsAtom* aName,
                                              nsAtom* aOther) {
  MOZ_ASSERT(mCurrentRun, "Adding error to run without one!");
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  opAddErrorType operation(mCurrentRun, (char*)aMsgId, aName, aOther);
  treeOp->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddErrorToCurrentAmpersand(const char* aMsgId) {
  MOZ_ASSERT(mAmpersand, "Adding error to ampersand without one!");
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  opAddErrorType operation(mAmpersand, (char*)aMsgId);
  treeOp->Init(mozilla::AsVariant(operation));
}

void nsHtml5Highlighter::AddErrorToCurrentSlash(const char* aMsgId) {
  MOZ_ASSERT(mSlash, "Adding error to slash without one!");
  nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
  NS_ASSERTION(treeOp, "Tree op allocation failed.");
  opAddErrorType operation(mSlash, (char*)aMsgId);
  treeOp->Init(mozilla::AsVariant(operation));
}
