/*
  This file is part of the Grantlee template system.

  Copyright (c) 2008 Stephen Kelly <steveire@gmail.com>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either version
  2.1 of the Licence, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "markupdirector.h"
#include "markupdirector_p.h"

#include "abstractmarkupbuilder.h"

#include <QtCore/QFlags>
#include <QtCore/QMap>
#include <QtCore/QStack>
#include <QtCore/QString>
#include <QtGui/QBrush>
#include <QtGui/QColor>
#include <QtGui/QTextCharFormat>
#include <QtGui/QTextCursor>
#include <QtGui/QTextDocument>
#include <QtGui/QTextDocumentFragment>
#include <QtGui/QTextFrame>
#include <QtGui/QTextList>
#include <QtGui/QTextTable>

using namespace Grantlee;

MarkupDirector::MarkupDirector(AbstractMarkupBuilder *builder)
    : d_ptr(new MarkupDirectorPrivate(this)), m_builder(builder)
{
}

MarkupDirector::~MarkupDirector() { delete d_ptr; }

void MarkupDirector::processDocumentContents(QTextFrame::iterator start,
                                             QTextFrame::iterator end)
{
  while (!start.atEnd() && start != end) {
    auto frame = start.currentFrame();
    if (frame) {
      auto table = qobject_cast<QTextTable *>(frame);
      if (table) {
        start = processTable(start, table);
      } else {
        start = processFrame(start, frame);
      }
    } else {
      auto block = start.currentBlock();
      Q_ASSERT(block.isValid());
      start = processBlock(start, block);
    }
  }
}

QTextFrame::iterator MarkupDirector::processFrame(QTextFrame::iterator it,
                                                  QTextFrame *frame)
{
  if (frame) {
    processDocumentContents(frame->begin(), frame->end());
  }
  if (!it.atEnd())
    return ++it;
  return it;
}

QTextFrame::iterator MarkupDirector::processBlock(QTextFrame::iterator it,
                                                  const QTextBlock &block)
{
  if (block.isValid()) {
    auto fmt = block.blockFormat();
    auto object = block.document()->objectForFormat(fmt);
    if (object) {
      return processObject(it, block, object);
    }
    return processBlockContents(it, block);
  }

  if (!it.atEnd())
    return ++it;
  return it;
}

QTextFrame::iterator MarkupDirector::processTable(QTextFrame::iterator it,
                                                  QTextTable *table)
{
  auto format = table->format();

  auto colLengths = format.columnWidthConstraints();

  auto tableWidth = format.width();
  QString sWidth;

  if (tableWidth.type() == QTextLength::PercentageLength) {
    sWidth = QStringLiteral("%1%");
    sWidth = sWidth.arg(tableWidth.rawValue());
  } else if (tableWidth.type() == QTextLength::FixedLength) {
    sWidth = QStringLiteral("%1");
    sWidth = sWidth.arg(tableWidth.rawValue());
  }

  m_builder->beginTable(format.cellPadding(), format.cellSpacing(), sWidth);

  auto headerRowCount = format.headerRowCount();

  QList<QTextTableCell> alreadyProcessedCells;

  for (auto row = 0; row < table->rows(); ++row) {
    // Put a thead element around here somewhere?
    // if (row < headerRowCount)
    // {
    // beginTableHeader();
    // }

    m_builder->beginTableRow();

    // Header attribute should really be on cells, not determined by number
    // of
    // rows.
    // http://www.webdesignfromscratch.com/html-tables.cfm

    for (auto column = 0; column < table->columns(); ++column) {

      auto tableCell = table->cellAt(row, column);

      auto columnSpan = tableCell.columnSpan();
      auto rowSpan = tableCell.rowSpan();
      if ((rowSpan > 1) || (columnSpan > 1)) {
        if (alreadyProcessedCells.contains(tableCell)) {
          // Already processed this cell. Move on.
          continue;
        }
        alreadyProcessedCells.append(tableCell);
      }

      auto cellWidth = colLengths.at(column);

      QString sCellWidth;

      if (cellWidth.type() == QTextLength::PercentageLength) {
        sCellWidth = QStringLiteral("%1%");
        sCellWidth = sCellWidth.arg(cellWidth.rawValue());
      } else if (cellWidth.type() == QTextLength::FixedLength) {
        sCellWidth = QStringLiteral("%1");
        sCellWidth = sCellWidth.arg(cellWidth.rawValue());
      }

      // TODO: Use THEAD instead
      if (row < headerRowCount) {
        m_builder->beginTableHeaderCell(sCellWidth, columnSpan, rowSpan);
      } else {
        m_builder->beginTableCell(sCellWidth, columnSpan, rowSpan);
      }

      processTableCell(tableCell, table);

      if (row < headerRowCount) {
        m_builder->endTableHeaderCell();
      } else {
        m_builder->endTableCell();
      }
    }
    m_builder->endTableRow();
  }
  m_builder->endTable();

  if (!it.atEnd())
    return ++it;
  return it;
}

void MarkupDirector::processTableCell(const QTextTableCell &tableCell,
                                      QTextTable *table)
{
  Q_UNUSED(table)
  processDocumentContents(tableCell.begin(), tableCell.end());
}

QPair<QTextFrame::iterator, QTextBlock>
MarkupDirector::processList(QTextFrame::iterator it, const QTextBlock &_block,
                            QTextList *list)
{
  auto style = list->format().style();
  m_builder->beginList(style);
  auto block = _block;
  auto curList = list;
  while (block.isValid() && block.textList() && curList == list) {
    m_builder->beginListItem();
    processBlockContents(it, block);
    m_builder->endListItem();

    if (!it.atEnd())
      ++it;
    block = block.next();
    // Processing nested list
    if (block.isValid() && block.textList()
        && block.textList()->format().indent() > list->format().indent()) {
      auto obj = block.document()->objectForFormat(block.blockFormat());
      auto group = qobject_cast<QTextBlockGroup *>(obj);
      if (group && group != list) {
        auto pair = processBlockGroup(it, block, group);
        it = pair.first;
        block = pair.second;
      }
    }
    curList = qobject_cast<QTextList *>(
        block.document()->objectForFormat(block.blockFormat()));
  }
  m_builder->endList();
  return qMakePair(it, block);
}

QTextFrame::iterator
MarkupDirector::processBlockContents(QTextFrame::iterator frameIt,
                                     const QTextBlock &block)
{
  auto blockFormat = block.blockFormat();
  auto blockAlignment = blockFormat.alignment();

  // TODO: decide when to use <h1> etc.

  if (blockFormat.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
    m_builder->insertHorizontalRule();
    if (!frameIt.atEnd())
      return ++frameIt;
    return frameIt;
  }

  auto it = block.begin();

  // The beginning is the end. This is an empty block. Insert a newline and
  // move
  // on.
  if (it.atEnd()) {
    m_builder->addNewline();
    if (!frameIt.atEnd())
      return ++frameIt;
    return frameIt;
  }

  // Don't have p tags inside li tags.
  if (!block.textList()) {
    // Don't instruct builders to use margins. The rich text widget doesn't
    // have
    // an action for them yet,
    // So users can't edit them. See bug
    // http://bugs.kde.org/show_bug.cgi?id=160600
    m_builder->beginParagraph(
        blockAlignment //,
        //                                blockFormat.topMargin(),
        //                                blockFormat.bottomMargin(),
        //                                blockFormat.leftMargin(),
        //                                blockFormat.rightMargin()
    );
  }

  while (!it.atEnd()) {
    it = processFragment(it, it.fragment(), block.document());
  }

  // Don't have p tags inside li tags.
  if (!block.textList()) {
    m_builder->endParagraph();
  }

  if (!frameIt.atEnd())
    return ++frameIt;
  return frameIt;
}

QTextBlock::iterator
MarkupDirector::processFragment(QTextBlock::iterator it,
                                const QTextFragment &fragment,
                                QTextDocument const *doc)
{
  //   Q_D( MarkupDirector );
  auto charFormat = fragment.charFormat();

  if (charFormat.objectType() >= QTextFormat::UserObject) {
    processCustomFragment(fragment, doc);
    if (!it.atEnd())
      return ++it;
    return it;
  }

  auto textObject = doc->objectForFormat(charFormat);
  if (textObject)
    return processCharTextObject(it, fragment, textObject);

  if (fragment.text().at(0).category() == QChar::Separator_Line) {
    m_builder->addNewline();

    if (!it.atEnd())
      return ++it;
    return it;
  }

  // The order of closing and opening tags can determine whether generated
  // html
  // is valid or not.
  // When processing a document with formatting which appears as
  // '<b><i>Some</i>
  // formatted<b> text',
  // the correct generated output will contain '<strong><em>Some</em>
  // formatted<strong> text'.
  // However, processing text which appears as '<i><b>Some</b> formatted<i>
  // text' might be incorrectly rendered
  // as '<strong><em>Some</strong> formatted</em> text' if tags which start at
  // the same fragment are
  // opened out of order. Here, tags are not nested properly, and the html
  // would
  // not be valid or render correctly by unforgiving parsers (like QTextEdit).
  // One solution is to make the order of opening tags dynamic. In the above
  // case, the em tag would
  // be opened before the strong tag '<em><strong>Some</strong> formatted</em>
  // text'. That would
  // require knowledge of which tag is going to close first. That might be
  // possible by examining
  // the 'next' QTextFragment while processing one.
  //
  // The other option is to do pessimistic closing of tags.
  // In the above case, this means that if a fragment has two or more formats
  // applied (bold and italic here),
  // and one of them is closed, then all tags should be closed first. They
  // will
  // of course be reopened
  // if necessary while processing the next fragment.
  // The above case would be rendered as '<strong><em>Some</em></strong><em>
  // formatted</em> text'.
  //
  // The first option is taken here, as the redundant opening and closing tags
  // in the second option
  // didn't appeal.
  // See testDoubleStartDifferentFinish,
  // testDoubleStartDifferentFinishReverseOrder

  processOpeningElements(it);

  // If a sequence such as '<br /><br />' is imported into a document with
  // setHtml, LineSeparator characters are inserted. Here I make sure to
  // put them back.
  auto sl = fragment.text().split(QChar(QChar::LineSeparator));
  auto i = sl.begin();
  auto end = sl.end();
  auto paraClosed = false;
  while (i != end) {
    m_builder->appendLiteralText(*i);
    ++i;
    if (i != end) {
      if (i->isEmpty()) {
        if (!paraClosed) {
          m_builder->endParagraph();
          paraClosed = true;
        }
        m_builder->addNewline();
      } else if (paraClosed) {
        m_builder->beginParagraph(/* blockAlignment */);
        paraClosed = false;
      }
    }
  }
  if (!it.atEnd())
    ++it;

  processClosingElements(it);

  return it;
}

void MarkupDirector::processCustomFragment(const QTextFragment &fragment,
                                           const QTextDocument *doc)
{
  Q_UNUSED(fragment)
  Q_UNUSED(doc)
}

QTextFrame::iterator MarkupDirector::processObject(QTextFrame::iterator it,
                                                   const QTextBlock &block,
                                                   QTextObject *object)
{
  auto group = qobject_cast<QTextBlockGroup *>(object);
  if (group) {
    return processBlockGroup(it, block, group).first;
  }
  if (!it.atEnd())
    return ++it;
  return it;
}

QPair<QTextFrame::iterator, QTextBlock>
MarkupDirector::skipBlockGroup(QTextFrame::iterator it,
                               const QTextBlock &_block,
                               QTextBlockGroup *blockGroup)
{
  auto block = _block;
  auto lastBlock = _block;
  auto lastIt = it;
  auto obj = block.document()->objectForFormat(block.blockFormat());
  QTextBlockGroup *nextGroup;

  if (!obj)
    return qMakePair(lastIt, lastBlock);

  auto group = qobject_cast<QTextBlockGroup *>(obj);
  if (!group)
    return qMakePair(lastIt, lastBlock);

  while (block.isValid()) {
    if (!group)
      break;

    block = block.next();
    if (!it.atEnd())
      ++it;

    obj = block.document()->objectForFormat(block.blockFormat());
    if (obj)
      continue;

    nextGroup = qobject_cast<QTextBlockGroup *>(obj);

    if (group == blockGroup || !nextGroup) {
      lastBlock = block;
      lastIt = it;
    }
    group = nextGroup;
  }
  return qMakePair(lastIt, lastBlock);
}

QPair<QTextFrame::iterator, QTextBlock>
MarkupDirector::processBlockGroup(QTextFrame::iterator it,
                                  const QTextBlock &block,
                                  QTextBlockGroup *blockGroup)
{
  auto list = qobject_cast<QTextList *>(blockGroup);
  if (list) {
    return processList(it, block, list);
  }
  return skipBlockGroup(it, block, blockGroup);
}

void MarkupDirector::processDocument(QTextDocument *doc)
{
  processFrame(QTextFrame::iterator(), doc->rootFrame());
}

QTextBlock::iterator
MarkupDirector::processCharTextObject(QTextBlock::iterator it,
                                      const QTextFragment &fragment,
                                      QTextObject *textObject)
{
  auto fragmentFormat = fragment.charFormat();
  if (fragmentFormat.isImageFormat()) {
    auto imageFormat = fragmentFormat.toImageFormat();
    return processImage(it, imageFormat, textObject->document());
  }
  if (!it.atEnd())
    return ++it;
  return it;
}

QTextBlock::iterator
MarkupDirector::processImage(QTextBlock::iterator it,
                             const QTextImageFormat &imageFormat,
                             QTextDocument *doc)
{
  Q_UNUSED(doc)
  // TODO: Close any open format elements?
  m_builder->insertImage(imageFormat.name(), imageFormat.width(),
                         imageFormat.height());
  if (!it.atEnd())
    return ++it;
  return it;
}

void MarkupDirector::processClosingElements(QTextBlock::iterator it)
{
  Q_D(MarkupDirector);
  // The order of closing elements is determined by the order they were opened
  // in.
  // The order of opened elements is in the openElements member list.
  // see testDifferentStartDoubleFinish and
  // testDifferentStartDoubleFinishReverseOrder

  if (d->m_openElements.isEmpty())
    return;

  auto elementsToClose = getElementsToClose(it);

  int previousSize;
  auto remainingSize = elementsToClose.size();
  while (!elementsToClose.isEmpty()) {
    auto tag = d->m_openElements.last();
    if (elementsToClose.contains(tag)) {
      switch (tag) {
      case Strong:
        m_builder->endStrong();
        break;
      case Emph:
        m_builder->endEmph();
        break;
      case Underline:
        m_builder->endUnderline();
        break;
      case StrikeOut:
        m_builder->endStrikeout();
        break;
      case SpanFontPointSize:
        m_builder->endFontPointSize();
        break;
      case SpanFontFamily:
        m_builder->endFontFamily();
        break;
      case SpanBackground:
        m_builder->endBackground();
        break;
      case SpanForeground:
        m_builder->endForeground();
        break;
      case Anchor:
        m_builder->endAnchor();
        break;
      case SubScript:
        m_builder->endSubscript();
        break;
      case SuperScript:
        m_builder->endSuperscript();
        break;

      default:
        break;
      }
      d->m_openElements.removeLast();
      elementsToClose.remove(tag);
    }
    previousSize = remainingSize;
    remainingSize = elementsToClose.size();

    if (previousSize == remainingSize) {
      // Iterated once through without closing any tags.
      // This means that there's overlap in the tags, such as
      // 'text with <b>some <i>formatting</i></b><i> tags</i>'
      // See testOverlap.
      // The top element in openElements must be a blocker, so close it on
      // next
      // iteration.
      elementsToClose.insert(d->m_openElements.last());
    }
  }
}

void MarkupDirector::processOpeningElements(QTextBlock::iterator it)
{
  Q_D(MarkupDirector);
  auto fragment = it.fragment();

  if (!fragment.isValid())
    return;

  auto fragmentFormat = fragment.charFormat();
  auto elementsToOpenList = getElementsToOpen(it);

  for (int tag : qAsConst(elementsToOpenList)) {
    switch (tag) {
    case Strong:
      m_builder->beginStrong();
      break;
    case Emph:
      m_builder->beginEmph();
      break;
    case Underline:
      m_builder->beginUnderline();
      break;
    case StrikeOut:
      m_builder->beginStrikeout();
      break;
    case SpanFontPointSize:
      m_builder->beginFontPointSize(fragmentFormat.font().pointSize());
      d->m_openFontPointSize = fragmentFormat.font().pointSize();
      break;
    case SpanFontFamily:
      m_builder->beginFontFamily(fragmentFormat.fontFamily());
      d->m_openFontFamily = fragmentFormat.fontFamily();
      break;
    case SpanBackground:
      m_builder->beginBackground(fragmentFormat.background());
      d->m_openBackground = fragmentFormat.background();
      break;
    case SpanForeground:
      m_builder->beginForeground(fragmentFormat.foreground());
      d->m_openForeground = fragmentFormat.foreground();
      break;
    case Anchor: {
      // TODO: Multiple anchor names here.
      auto anchorNames = fragmentFormat.anchorNames();
      if (!anchorNames.isEmpty()) {
        while (!anchorNames.isEmpty()) {
          auto n = anchorNames.last();
          anchorNames.removeLast();
          if (anchorNames.isEmpty()) {
            // Doesn't matter if anchorHref is empty.
            m_builder->beginAnchor(fragmentFormat.anchorHref(), n);
            break;
          } // Empty <a> tags allow multiple names for the same
          // section.
          m_builder->beginAnchor(QString(), n);
          m_builder->endAnchor();
        }
      } else {
        m_builder->beginAnchor(fragmentFormat.anchorHref());
      }
      d->m_openAnchorHref = fragmentFormat.anchorHref();
      break;
    }
    case SuperScript:
      m_builder->beginSuperscript();
      break;
    case SubScript:
      m_builder->beginSubscript();
      break;
    default:
      break;
    }
    d->m_openElements.append(tag);
    d->m_elementsToOpen.remove(tag);
  }
}

QSet<int> MarkupDirector::getElementsToClose(QTextBlock::iterator it) const
{
  Q_D(const MarkupDirector);
  QSet<int> closedElements;

  if (it.atEnd()) {
    // End of block?. Close all open tags.
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
    auto elementsToClose = d->m_openElements.toSet();
#else
    auto elementsToClose
        = QSet<int>(d->m_openElements.begin(), d->m_openElements.end());
#endif
    return elementsToClose.unite(d->m_elementsToOpen);
  }

  auto fragment = it.fragment();

  if (!fragment.isValid())
    return closedElements;

  auto fragmentFormat = fragment.charFormat();

  auto fontWeight = fragmentFormat.fontWeight();
  auto fontItalic = fragmentFormat.fontItalic();
  auto fontUnderline = fragmentFormat.fontUnderline();
  auto fontStrikeout = fragmentFormat.fontStrikeOut();

  auto fontForeground = fragmentFormat.foreground();
  auto fontBackground = fragmentFormat.background();

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  auto fontFamily = fragmentFormat.fontFamily();
#else
  QString fontFamily;
  auto fontFamilies = fragmentFormat.fontFamilies();
  if (fontFamilies.isValid()) {
    auto fflist = fontFamilies.value<QList<QString>>();
    if (!fflist.isEmpty()) {
      fontFamily = fflist[0];
    }
  }
#endif
  auto fontPointSize = fragmentFormat.font().pointSize();
  auto anchorHref = fragmentFormat.anchorHref();

  auto vAlign = fragmentFormat.verticalAlignment();
  auto superscript = (vAlign == QTextCharFormat::AlignSuperScript);
  auto subscript = (vAlign == QTextCharFormat::AlignSubScript);

  if (!fontStrikeout
      && (d->m_openElements.contains(StrikeOut)
          || d->m_elementsToOpen.contains(StrikeOut))) {
    closedElements.insert(StrikeOut);
  }

  if (!fontUnderline
      && (d->m_openElements.contains(Underline)
          || d->m_elementsToOpen.contains(Underline))
      && !(d->m_openElements.contains(Anchor)
           || d->m_elementsToOpen.contains(Anchor))) {
    closedElements.insert(Underline);
  }

  if (!fontItalic
      && (d->m_openElements.contains(Emph)
          || d->m_elementsToOpen.contains(Emph))) {
    closedElements.insert(Emph);
  }

  if (fontWeight != QFont::Bold
      && (d->m_openElements.contains(Strong)
          || d->m_elementsToOpen.contains(Strong))) {
    closedElements.insert(Strong);
  }

  if ((d->m_openElements.contains(SpanFontPointSize)
       || d->m_elementsToOpen.contains(SpanFontPointSize))
      && (d->m_openFontPointSize != fontPointSize)) {
    closedElements.insert(SpanFontPointSize);
  }

  if ((d->m_openElements.contains(SpanFontFamily)
       || d->m_elementsToOpen.contains(SpanFontFamily))
      && (d->m_openFontFamily != fontFamily)) {
    closedElements.insert(SpanFontFamily);
  }

  if ((d->m_openElements.contains(SpanBackground)
       && (d->m_openBackground != fontBackground))
      || (d->m_elementsToOpen.contains(SpanBackground)
          && (d->m_backgroundToOpen != fontBackground))) {
    closedElements.insert(SpanBackground);
  }

  if ((d->m_openElements.contains(SpanForeground)
       && (d->m_openForeground != fontForeground))
      || (d->m_elementsToOpen.contains(SpanForeground)
          && (d->m_foregroundToOpen != fontForeground))) {
    closedElements.insert(SpanForeground);
  }

  if ((d->m_openElements.contains(Anchor)
       && (d->m_openAnchorHref != anchorHref))
      || (d->m_elementsToOpen.contains(Anchor)
          && (d->m_anchorHrefToOpen != anchorHref))) {
    closedElements.insert(Anchor);
  }

  if (!subscript
      && (d->m_openElements.contains(SubScript)
          || d->m_elementsToOpen.contains(SubScript))) {
    closedElements.insert(SubScript);
  }

  if (!superscript
      && (d->m_openElements.contains(SuperScript)
          || d->m_elementsToOpen.contains(SuperScript))) {
    closedElements.insert(SuperScript);
  }
  return closedElements;
}

QList<int> MarkupDirector::getElementsToOpen(QTextBlock::iterator it)
{
  Q_D(MarkupDirector);
  auto fragment = it.fragment();
  if (!fragment.isValid()) {
    return {};
  }
  auto fragmentFormat = fragment.charFormat();

  auto fontWeight = fragmentFormat.fontWeight();
  auto fontItalic = fragmentFormat.fontItalic();
  auto fontUnderline = fragmentFormat.fontUnderline();
  auto fontStrikeout = fragmentFormat.fontStrikeOut();

  auto fontForeground = fragmentFormat.foreground();
  auto fontBackground = fragmentFormat.background();

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
  auto fontFamily = fragmentFormat.fontFamily();
#else
  QString fontFamily;
  auto fontFamilies = fragmentFormat.fontFamilies();
  if (fontFamilies.isValid()) {
    auto fflist = fontFamilies.value<QList<QString>>();
    if (!fflist.isEmpty()) {
      fontFamily = fflist[0];
    }
  }
#endif
  auto fontPointSize = fragmentFormat.font().pointSize();
  auto anchorHref = fragmentFormat.anchorHref();

  auto vAlign = fragmentFormat.verticalAlignment();
  auto superscript = (vAlign == QTextCharFormat::AlignSuperScript);
  auto subscript = (vAlign == QTextCharFormat::AlignSubScript);

  if (superscript && !(d->m_openElements.contains(SuperScript))) {
    d->m_elementsToOpen.insert(SuperScript);
  }

  if (subscript && !(d->m_openElements.contains(SubScript))) {
    d->m_elementsToOpen.insert(SubScript);
  }

  if (!anchorHref.isEmpty() && !(d->m_openElements.contains(Anchor))
      && (d->m_openAnchorHref != anchorHref)) {
    d->m_elementsToOpen.insert(Anchor);
    d->m_anchorHrefToOpen = anchorHref;
  }

  if (fontForeground != Qt::NoBrush
      && !(d->m_openElements.contains(SpanForeground)) // Can only open one
                                                       // foreground element
                                                       // at a time.
      && (fontForeground != d->m_openForeground)
      && !((d->m_openElements.contains(
                Anchor) // Links can't have a foreground color.
            || d->m_elementsToOpen.contains(Anchor)))) {
    d->m_elementsToOpen.insert(SpanForeground);
    d->m_foregroundToOpen = fontForeground;
  }

  if (fontBackground != Qt::NoBrush
      && !(d->m_openElements.contains(SpanBackground))
      && (fontBackground != d->m_openBackground)) {
    d->m_elementsToOpen.insert(SpanBackground);
    d->m_backgroundToOpen = fontBackground;
  }

  if (!fontFamily.isEmpty() && !(d->m_openElements.contains(SpanFontFamily))
      && (fontFamily != d->m_openFontFamily)) {
    d->m_elementsToOpen.insert(SpanFontFamily);
    d->m_fontFamilyToOpen = fontFamily;
  }

  if ((QTextCharFormat().font().pointSize()
       != fontPointSize) // Different from the default.
      && !(d->m_openElements.contains(SpanFontPointSize))
      && (fontPointSize != d->m_openFontPointSize)) {
    d->m_elementsToOpen.insert(SpanFontPointSize);
    d->m_fontPointSizeToOpen = fontPointSize;
  }

  //   Only open a new bold tag if one is not already open.
  //   eg, <b>some <i>mixed</i> format</b> should be as is, rather than
  //   <b>some </b><b><i>mixed</i></b><b> format</b>

  if (fontWeight == QFont::Bold && !(d->m_openElements.contains(Strong))) {
    d->m_elementsToOpen.insert(Strong);
  }

  if (fontItalic && !(d->m_openElements.contains(Emph))) {
    d->m_elementsToOpen.insert(Emph);
  }

  if (fontUnderline && !(d->m_openElements.contains(Underline))
      && !(d->m_openElements.contains(Anchor)
           || d->m_elementsToOpen.contains(
               Anchor)) // Can't change the underline state of a link.
  ) {
    d->m_elementsToOpen.insert(Underline);
  }

  if (fontStrikeout && !(d->m_openElements.contains(StrikeOut))) {
    d->m_elementsToOpen.insert(StrikeOut);
  }

  if (d->m_elementsToOpen.size() <= 1) {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
    return d->m_elementsToOpen.toList();
#else
    return QList<int>(d->m_elementsToOpen.begin(), d->m_elementsToOpen.end());
#endif
  }
  return sortOpeningOrder(d->m_elementsToOpen, it);
}

QList<int> MarkupDirector::sortOpeningOrder(QSet<int> openingOrder,
                                            QTextBlock::iterator it) const
{
  QList<int> sortedOpenedElements;

  // This is an insertion sort in a way. elements in openingOrder are assumed
  // to
  // be out of order.
  // The rest of the block is traversed until there are no more elements to
  // sort, or the end is reached.
  while (openingOrder.size() != 0) {
    if (!it.atEnd()) {
      it++;

      if (!it.atEnd()) {
        // Because I've iterated, this returns the elements that will
        // be closed by the next fragment.
        auto elementsToClose = getElementsToClose(it);

        // The exact order these are opened in is irrelevant, as all
        // will be
        // closed on the same block.
        // See testDoubleFormat.
        for (int tag : qAsConst(elementsToClose)) {
          if (openingOrder.remove(tag)) {
            sortedOpenedElements.prepend(tag);
          }
        }
      }
    } else {
      // End of block. Need to close all open elements.
      // Order irrelevant in this case.
      for (int tag : openingOrder) {
        sortedOpenedElements.prepend(tag);
      }
      break;
    }
  }
  return sortedOpenedElements;
}
