File indexing completed on 2024-05-19 05:21:46
0001 /* 0002 SPDX-FileCopyrightText: 2019-2020 Montel Laurent <montel@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 0006 */ 0007 0008 #include "markupdirector.h" 0009 #include "markupdirector_p.h" 0010 0011 #include "abstractmarkupbuilder.h" 0012 0013 #include <QBrush> 0014 #include <QColor> 0015 #include <QFlags> 0016 #include <QMap> 0017 #include <QStack> 0018 #include <QString> 0019 #include <QTextCharFormat> 0020 #include <QTextCursor> 0021 #include <QTextDocument> 0022 #include <QTextDocumentFragment> 0023 #include <QTextFrame> 0024 #include <QTextList> 0025 #include <QTextTable> 0026 0027 #include <QDebug> 0028 using namespace KPIMTextEdit; 0029 0030 MarkupDirector::MarkupDirector(KPIMTextEdit::AbstractMarkupBuilder *builder) 0031 : d_ptr(new MarkupDirectorPrivate(this)) 0032 , m_builder(builder) 0033 { 0034 } 0035 0036 MarkupDirector::~MarkupDirector() 0037 { 0038 delete d_ptr; 0039 } 0040 0041 // #define ADD_HEADER_SUPPORT 1 0042 0043 QTextFrame::iterator MarkupDirector::processBlockContents(QTextFrame::iterator frameIt, const QTextBlock &block) 0044 { 0045 // Same code as grantlee but interpret margin 0046 0047 const auto blockFormat = block.blockFormat(); 0048 const auto blockAlignment = blockFormat.alignment(); 0049 const bool leftToRightText = block.textDirection() == Qt::RightToLeft; 0050 0051 // TODO: decide when to use <h1> etc. 0052 #ifdef ADD_HEADER_SUPPORT 0053 if (blockFormat.headingLevel() > 0) { 0054 // Header 0055 qDebug() << " header " << blockFormat.headingLevel(); 0056 m_builder->beginHeader(blockFormat.headingLevel()); 0057 } 0058 #endif 0059 if (blockFormat.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) { 0060 m_builder->insertHorizontalRule(); 0061 if (!frameIt.atEnd()) { 0062 return ++frameIt; 0063 } 0064 return frameIt; 0065 } 0066 0067 auto it = block.begin(); 0068 0069 // The beginning is the end. This is an empty block. Insert a newline and 0070 // move 0071 // on. 0072 if (it.atEnd()) { 0073 m_builder->addSingleBreakLine(); 0074 0075 if (!frameIt.atEnd()) { 0076 return ++frameIt; 0077 } 0078 return frameIt; 0079 } 0080 0081 // Don't have p tags inside li tags. 0082 if (!block.textList()) { 0083 // Laurent : we need this margin as it's necessary to show blockquote 0084 0085 // Don't instruct builders to use margins. The rich text widget doesn't 0086 // have 0087 // an action for them yet, 0088 // So users can't edit them. See bug 0089 // https://bugs.kde.org/show_bug.cgi?id=160600 0090 m_builder->beginParagraph(blockAlignment, 0091 blockFormat.topMargin(), 0092 blockFormat.bottomMargin(), 0093 blockFormat.leftMargin(), 0094 blockFormat.rightMargin(), 0095 leftToRightText); 0096 } 0097 0098 while (!it.atEnd()) { 0099 it = processFragment(it, it.fragment(), block.document()); 0100 } 0101 // Don't have p tags inside li tags. 0102 if (!block.textList()) { 0103 m_builder->endParagraph(); 0104 } 0105 0106 if (!frameIt.atEnd()) { 0107 return ++frameIt; 0108 } 0109 return frameIt; 0110 } 0111 0112 QTextBlock::iterator MarkupDirector::processFragment(QTextBlock::iterator it, const QTextFragment &fragment, QTextDocument const *doc) 0113 { 0114 // Same code as Grantlee + a fix ! 0115 0116 // Q_D( MarkupDirector ); 0117 const auto charFormat = fragment.charFormat(); 0118 // Need to check if it's a image format. 0119 if (charFormat.isImageFormat()) { 0120 const auto imageFormat = charFormat.toImageFormat(); 0121 return processImage(it, imageFormat, doc); 0122 } 0123 0124 if (charFormat.objectType() >= QTextFormat::UserObject) { 0125 processCustomFragment(fragment, doc); 0126 if (!it.atEnd()) { 0127 return ++it; 0128 } 0129 return it; 0130 } 0131 0132 auto textObject = doc->objectForFormat(charFormat); 0133 if (textObject) { 0134 return processCharTextObject(it, fragment, textObject); 0135 } 0136 0137 const auto textStr = fragment.text(); 0138 if (textStr.at(0).category() == QChar::Separator_Line) { 0139 m_builder->addSingleBreakLine(); 0140 QString t; 0141 for (int i = 1; i < textStr.length(); ++i) { 0142 if (fragment.text().at(i).category() == QChar::Separator_Line) { 0143 m_builder->appendLiteralText(t); 0144 if (i < textStr.length() - 1) { // Don't add \n when we have the last char 0145 m_builder->addSingleBreakLine(); 0146 } 0147 t.clear(); 0148 } else { 0149 t += fragment.text().at(i); 0150 } 0151 } 0152 if (!t.isEmpty()) { 0153 m_builder->appendLiteralText(t); 0154 } 0155 0156 if (!it.atEnd()) { 0157 return ++it; 0158 } 0159 return it; 0160 } 0161 0162 // The order of closing and opening tags can determine whether generated 0163 // html 0164 // is valid or not. 0165 // When processing a document with formatting which appears as 0166 // '<b><i>Some</i> 0167 // formatted<b> text', 0168 // the correct generated output will contain '<strong><em>Some</em> 0169 // formatted<strong> text'. 0170 // However, processing text which appears as '<i><b>Some</b> formatted<i> 0171 // text' might be incorrectly rendered 0172 // as '<strong><em>Some</strong> formatted</em> text' if tags which start at 0173 // the same fragment are 0174 // opened out of order. Here, tags are not nested properly, and the html 0175 // would 0176 // not be valid or render correctly by unforgiving parsers (like QTextEdit). 0177 // One solution is to make the order of opening tags dynamic. In the above 0178 // case, the em tag would 0179 // be opened before the strong tag '<em><strong>Some</strong> formatted</em> 0180 // text'. That would 0181 // require knowledge of which tag is going to close first. That might be 0182 // possible by examining 0183 // the 'next' QTextFragment while processing one. 0184 // 0185 // The other option is to do pessimistic closing of tags. 0186 // In the above case, this means that if a fragment has two or more formats 0187 // applied (bold and italic here), 0188 // and one of them is closed, then all tags should be closed first. They 0189 // will 0190 // of course be reopened 0191 // if necessary while processing the next fragment. 0192 // The above case would be rendered as '<strong><em>Some</em></strong><em> 0193 // formatted</em> text'. 0194 // 0195 // The first option is taken here, as the redundant opening and closing tags 0196 // in the second option 0197 // didn't appeal. 0198 // See testDoubleStartDifferentFinish, 0199 // testDoubleStartDifferentFinishReverseOrder 0200 0201 processOpeningElements(it); 0202 0203 // If a sequence such as '<br /><br />' is imported into a document with 0204 // setHtml, LineSeparator 0205 // characters are inserted. Here I make sure to put them back. 0206 auto sl = fragment.text().split(QChar(QChar::LineSeparator)); 0207 QStringListIterator i(sl); 0208 auto paraClosed = false; 0209 while (i.hasNext()) { 0210 m_builder->appendLiteralText(i.next()); 0211 if (i.hasNext()) { 0212 if (i.peekNext().isEmpty()) { 0213 if (!paraClosed) { 0214 m_builder->endParagraph(); 0215 paraClosed = true; 0216 } 0217 m_builder->addNewline(); 0218 } else if (paraClosed) { 0219 m_builder->beginParagraph(/* blockAlignment */); 0220 paraClosed = false; 0221 } else { 0222 // Bug fixing : add missing single break line 0223 m_builder->addSingleBreakLine(); 0224 } 0225 } 0226 } 0227 if (!it.atEnd()) { 0228 ++it; 0229 } 0230 0231 processClosingElements(it); 0232 0233 return it; 0234 } 0235 0236 void MarkupDirector::processDocumentContents(QTextFrame::iterator start, const QTextFrame::iterator &end) 0237 { 0238 while (!start.atEnd() && start != end) { 0239 auto frame = start.currentFrame(); 0240 if (frame) { 0241 auto table = qobject_cast<QTextTable *>(frame); 0242 if (table) { 0243 start = processTable(start, table); 0244 } else { 0245 start = processFrame(start, frame); 0246 } 0247 } else { 0248 auto block = start.currentBlock(); 0249 Q_ASSERT(block.isValid()); 0250 start = processBlock(start, block); 0251 } 0252 } 0253 } 0254 0255 QTextFrame::iterator MarkupDirector::processFrame(QTextFrame::iterator it, QTextFrame *frame) 0256 { 0257 if (frame) { 0258 processDocumentContents(frame->begin(), frame->end()); 0259 } 0260 if (!it.atEnd()) { 0261 return ++it; 0262 } 0263 return it; 0264 } 0265 0266 QTextFrame::iterator MarkupDirector::processBlock(QTextFrame::iterator it, const QTextBlock &block) 0267 { 0268 if (block.isValid()) { 0269 auto fmt = block.blockFormat(); 0270 auto object = block.document()->objectForFormat(fmt); 0271 if (object) { 0272 return processObject(it, block, object); 0273 } else { 0274 return processBlockContents(it, block); 0275 } 0276 } 0277 0278 if (!it.atEnd()) { 0279 return ++it; 0280 } 0281 return it; 0282 } 0283 0284 QTextFrame::iterator MarkupDirector::processTable(QTextFrame::iterator it, QTextTable *table) 0285 { 0286 const auto format = table->format(); 0287 0288 const auto colLengths = format.columnWidthConstraints(); 0289 0290 const auto tableWidth = format.width(); 0291 QString sWidth; 0292 0293 if (tableWidth.type() == QTextLength::PercentageLength) { 0294 sWidth = QStringLiteral("%1%"); 0295 sWidth = sWidth.arg(tableWidth.rawValue()); 0296 } else if (tableWidth.type() == QTextLength::FixedLength) { 0297 sWidth = QStringLiteral("%1"); 0298 sWidth = sWidth.arg(tableWidth.rawValue()); 0299 } 0300 0301 m_builder->beginTable(format.cellPadding(), format.cellSpacing(), sWidth); 0302 0303 const auto headerRowCount = format.headerRowCount(); 0304 0305 QList<QTextTableCell> alreadyProcessedCells; 0306 0307 for (int row = 0, total = table->rows(); row < total; ++row) { 0308 // Put a thead element around here somewhere? 0309 // if (row < headerRowCount) 0310 // { 0311 // beginTableHeader(); 0312 // } 0313 0314 m_builder->beginTableRow(); 0315 0316 // Header attribute should really be on cells, not determined by number 0317 // of 0318 // rows. 0319 // http://www.webdesignfromscratch.com/html-tables.cfm 0320 0321 for (int column = 0, total = table->columns(); column < total; ++column) { 0322 auto tableCell = table->cellAt(row, column); 0323 0324 auto columnSpan = tableCell.columnSpan(); 0325 auto rowSpan = tableCell.rowSpan(); 0326 if ((rowSpan > 1) || (columnSpan > 1)) { 0327 if (alreadyProcessedCells.contains(tableCell)) { 0328 // Already processed this cell. Move on. 0329 continue; 0330 } else { 0331 alreadyProcessedCells.append(tableCell); 0332 } 0333 } 0334 0335 auto cellWidth = colLengths.at(column); 0336 0337 QString sCellWidth; 0338 0339 if (cellWidth.type() == QTextLength::PercentageLength) { 0340 sCellWidth = QStringLiteral("%1%").arg(cellWidth.rawValue()); 0341 } else if (cellWidth.type() == QTextLength::FixedLength) { 0342 sCellWidth = QStringLiteral("%1").arg(cellWidth.rawValue()); 0343 } 0344 0345 // TODO: Use THEAD instead 0346 if (row < headerRowCount) { 0347 m_builder->beginTableHeaderCell(sCellWidth, columnSpan, rowSpan); 0348 } else { 0349 m_builder->beginTableCell(sCellWidth, columnSpan, rowSpan); 0350 } 0351 0352 processTableCell(tableCell, table); 0353 0354 if (row < headerRowCount) { 0355 m_builder->endTableHeaderCell(); 0356 } else { 0357 m_builder->endTableCell(); 0358 } 0359 } 0360 m_builder->endTableRow(); 0361 } 0362 m_builder->endTable(); 0363 0364 if (!it.atEnd()) { 0365 return ++it; 0366 } 0367 return it; 0368 } 0369 0370 void MarkupDirector::processTableCell(const QTextTableCell &tableCell, QTextTable *table) 0371 { 0372 Q_UNUSED(table) 0373 processDocumentContents(tableCell.begin(), tableCell.end()); 0374 } 0375 0376 QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::processList(QTextFrame::iterator it, const QTextBlock &_block, QTextList *list) 0377 { 0378 const auto style = list->format().style(); 0379 m_builder->beginList(style); 0380 auto block = _block; 0381 while (block.isValid() && block.textList()) { 0382 m_builder->beginListItem(); 0383 processBlockContents(it, block); 0384 m_builder->endListItem(); 0385 0386 if (!it.atEnd()) { 0387 ++it; 0388 } 0389 block = block.next(); 0390 if (block.isValid()) { 0391 auto obj = block.document()->objectForFormat(block.blockFormat()); 0392 auto group = qobject_cast<QTextBlockGroup *>(obj); 0393 if (group && group != list) { 0394 auto pair = processBlockGroup(it, block, group); 0395 it = pair.first; 0396 block = pair.second; 0397 } 0398 } 0399 } 0400 m_builder->endList(); 0401 return qMakePair(it, block); 0402 } 0403 0404 void MarkupDirector::processCustomFragment(const QTextFragment &fragment, const QTextDocument *doc) 0405 { 0406 Q_UNUSED(fragment) 0407 Q_UNUSED(doc) 0408 } 0409 0410 QTextFrame::iterator MarkupDirector::processObject(QTextFrame::iterator it, const QTextBlock &block, QTextObject *object) 0411 { 0412 auto group = qobject_cast<QTextBlockGroup *>(object); 0413 if (group) { 0414 return processBlockGroup(it, block, group).first; 0415 } 0416 if (!it.atEnd()) { 0417 return ++it; 0418 } 0419 return it; 0420 } 0421 0422 QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::skipBlockGroup(QTextFrame::iterator it, const QTextBlock &_block, QTextBlockGroup *blockGroup) 0423 { 0424 auto block = _block; 0425 auto lastBlock = _block; 0426 auto lastIt = it; 0427 auto obj = block.document()->objectForFormat(block.blockFormat()); 0428 QTextBlockGroup *nextGroup; 0429 0430 if (!obj) { 0431 return qMakePair(lastIt, lastBlock); 0432 } 0433 0434 auto group = qobject_cast<QTextBlockGroup *>(obj); 0435 if (!group) { 0436 return qMakePair(lastIt, lastBlock); 0437 } 0438 0439 while (block.isValid()) { 0440 if (!group) { 0441 break; 0442 } 0443 0444 block = block.next(); 0445 if (!it.atEnd()) { 0446 ++it; 0447 } 0448 0449 obj = block.document()->objectForFormat(block.blockFormat()); 0450 if (obj) { 0451 continue; 0452 } 0453 0454 nextGroup = qobject_cast<QTextBlockGroup *>(obj); 0455 0456 if (group == blockGroup || !nextGroup) { 0457 lastBlock = block; 0458 lastIt = it; 0459 } 0460 group = nextGroup; 0461 } 0462 return qMakePair(lastIt, lastBlock); 0463 } 0464 0465 QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::processBlockGroup(const QTextFrame::iterator &it, const QTextBlock &block, QTextBlockGroup *blockGroup) 0466 { 0467 const auto list = qobject_cast<QTextList *>(blockGroup); 0468 if (list) { 0469 return processList(it, block, list); 0470 } 0471 return skipBlockGroup(it, block, blockGroup); 0472 } 0473 0474 void MarkupDirector::processDocument(QTextDocument *doc) 0475 { 0476 processFrame(QTextFrame::iterator(), doc->rootFrame()); 0477 } 0478 0479 QTextBlock::iterator MarkupDirector::processCharTextObject(QTextBlock::iterator it, const QTextFragment &fragment, QTextObject *textObject) 0480 { 0481 const auto fragmentFormat = fragment.charFormat(); 0482 if (fragmentFormat.isImageFormat()) { 0483 const auto imageFormat = fragmentFormat.toImageFormat(); 0484 return processImage(it, imageFormat, textObject->document()); 0485 } 0486 if (!it.atEnd()) { 0487 return ++it; 0488 } 0489 return it; 0490 } 0491 0492 QTextBlock::iterator MarkupDirector::processImage(QTextBlock::iterator it, const QTextImageFormat &imageFormat, const QTextDocument *doc) 0493 { 0494 Q_UNUSED(doc) 0495 // TODO: Close any open format elements? 0496 m_builder->insertImage(imageFormat.name(), imageFormat.width(), imageFormat.height()); 0497 if (!it.atEnd()) { 0498 return ++it; 0499 } 0500 return it; 0501 } 0502 0503 void MarkupDirector::processClosingElements(const QTextBlock::iterator &it) 0504 { 0505 Q_D(MarkupDirector); 0506 // The order of closing elements is determined by the order they were opened 0507 // in. 0508 // The order of opened elements is in the openElements member list. 0509 // see testDifferentStartDoubleFinish and 0510 // testDifferentStartDoubleFinishReverseOrder 0511 0512 if (d->m_openElements.isEmpty()) { 0513 return; 0514 } 0515 0516 auto elementsToClose = getElementsToClose(it); 0517 0518 int previousSize; 0519 auto remainingSize = elementsToClose.size(); 0520 while (!elementsToClose.isEmpty()) { 0521 auto tag = d->m_openElements.last(); 0522 if (elementsToClose.contains(tag)) { 0523 switch (tag) { 0524 case Strong: 0525 m_builder->endStrong(); 0526 break; 0527 case Emph: 0528 m_builder->endEmph(); 0529 break; 0530 case Underline: 0531 m_builder->endUnderline(); 0532 break; 0533 case StrikeOut: 0534 m_builder->endStrikeout(); 0535 break; 0536 case SpanFontPointSize: 0537 m_builder->endFontPointSize(); 0538 break; 0539 case SpanFontFamily: 0540 m_builder->endFontFamily(); 0541 break; 0542 case SpanBackground: 0543 m_builder->endBackground(); 0544 // Clear background otherwise if we select 2 lines with same color it will not applied. bug #442416 0545 d->m_openBackground = {}; 0546 break; 0547 case SpanForeground: 0548 m_builder->endForeground(); 0549 // Clear foreground text color otherwise if we select 2 lines with same text color it will not applied. bug #442416 0550 d->m_openForeground = {}; 0551 break; 0552 case Anchor: 0553 m_builder->endAnchor(); 0554 break; 0555 case SubScript: 0556 m_builder->endSubscript(); 0557 break; 0558 case SuperScript: 0559 m_builder->endSuperscript(); 0560 break; 0561 0562 default: 0563 break; 0564 } 0565 d->m_openElements.removeLast(); 0566 elementsToClose.remove(tag); 0567 } 0568 previousSize = remainingSize; 0569 remainingSize = elementsToClose.size(); 0570 0571 if (previousSize == remainingSize) { 0572 // Iterated once through without closing any tags. 0573 // This means that there's overlap in the tags, such as 0574 // 'text with <b>some <i>formatting</i></b><i> tags</i>' 0575 // See testOverlap. 0576 // The top element in openElements must be a blocker, so close it on 0577 // next 0578 // iteration. 0579 elementsToClose.insert(d->m_openElements.last()); 0580 } 0581 } 0582 } 0583 0584 void MarkupDirector::processOpeningElements(const QTextBlock::iterator &it) 0585 { 0586 Q_D(MarkupDirector); 0587 auto fragment = it.fragment(); 0588 0589 if (!fragment.isValid()) { 0590 return; 0591 } 0592 0593 const auto fragmentFormat = fragment.charFormat(); 0594 const auto elementsToOpenList = getElementsToOpen(it); 0595 0596 for (int tag : elementsToOpenList) { 0597 switch (tag) { 0598 case Strong: 0599 m_builder->beginStrong(); 0600 break; 0601 case Emph: 0602 m_builder->beginEmph(); 0603 break; 0604 case Underline: 0605 m_builder->beginUnderline(); 0606 break; 0607 case StrikeOut: 0608 m_builder->beginStrikeout(); 0609 break; 0610 case SpanFontPointSize: 0611 d->m_openFontPointSize = fragmentFormat.font().pointSize(); 0612 m_builder->beginFontPointSize(d->m_openFontPointSize); 0613 break; 0614 case SpanFontFamily: 0615 d->m_openFontFamily = 0616 fragmentFormat.fontFamilies().toStringList().isEmpty() ? QString() : fragmentFormat.fontFamilies().toStringList().constFirst(); 0617 m_builder->beginFontFamily(d->m_openFontFamily); 0618 break; 0619 case SpanBackground: 0620 d->m_openBackground = fragmentFormat.background(); 0621 m_builder->beginBackground(d->m_openBackground); 0622 break; 0623 case SpanForeground: 0624 d->m_openForeground = fragmentFormat.foreground(); 0625 m_builder->beginForeground(d->m_openForeground); 0626 break; 0627 case Anchor: { 0628 // TODO: Multiple anchor names here. 0629 auto anchorNames = fragmentFormat.anchorNames(); 0630 if (!anchorNames.isEmpty()) { 0631 while (!anchorNames.isEmpty()) { 0632 auto n = anchorNames.last(); 0633 anchorNames.removeLast(); 0634 if (anchorNames.isEmpty()) { 0635 // Doesn't matter if anchorHref is empty. 0636 m_builder->beginAnchor(fragmentFormat.anchorHref(), n); 0637 break; 0638 } else { 0639 // Empty <a> tags allow multiple names for the same 0640 // section. 0641 m_builder->beginAnchor(QString(), n); 0642 m_builder->endAnchor(); 0643 } 0644 } 0645 } else { 0646 m_builder->beginAnchor(fragmentFormat.anchorHref()); 0647 } 0648 d->m_openAnchorHref = fragmentFormat.anchorHref(); 0649 break; 0650 } 0651 case SuperScript: 0652 m_builder->beginSuperscript(); 0653 break; 0654 case SubScript: 0655 m_builder->beginSubscript(); 0656 break; 0657 default: 0658 break; 0659 } 0660 d->m_openElements.append(tag); 0661 d->m_elementsToOpen.remove(tag); 0662 } 0663 } 0664 0665 QSet<int> MarkupDirector::getElementsToClose(const QTextBlock::iterator &it) const 0666 { 0667 Q_D(const MarkupDirector); 0668 QSet<int> closedElements; 0669 0670 if (it.atEnd()) { 0671 // End of block?. Close all open tags. 0672 0673 const QList<int> openElement = d->m_openElements; 0674 auto elementsToClose = QSet<int>(openElement.begin(), openElement.end()); 0675 return elementsToClose.unite(d->m_elementsToOpen); 0676 } 0677 0678 auto fragment = it.fragment(); 0679 0680 if (!fragment.isValid()) { 0681 return closedElements; 0682 } 0683 0684 const auto fragmentFormat = fragment.charFormat(); 0685 const auto fontWeight = fragmentFormat.fontWeight(); 0686 const auto fontItalic = fragmentFormat.fontItalic(); 0687 const auto fontUnderline = fragmentFormat.fontUnderline(); 0688 const auto fontStrikeout = fragmentFormat.fontStrikeOut(); 0689 0690 const auto fontForeground = fragmentFormat.foreground(); 0691 const auto fontBackground = fragmentFormat.background(); 0692 const QStringList families = fragmentFormat.fontFamilies().toStringList(); 0693 const auto fontFamily = families.isEmpty() ? QString() : families.constFirst(); 0694 const auto fontPointSize = fragmentFormat.font().pointSize(); 0695 const auto anchorHref = fragmentFormat.anchorHref(); 0696 0697 const auto vAlign = fragmentFormat.verticalAlignment(); 0698 const auto superscript = (vAlign == QTextCharFormat::AlignSuperScript); 0699 const auto subscript = (vAlign == QTextCharFormat::AlignSubScript); 0700 0701 if (!fontStrikeout && (d->m_openElements.contains(StrikeOut) || d->m_elementsToOpen.contains(StrikeOut))) { 0702 closedElements.insert(StrikeOut); 0703 } 0704 0705 if (!fontUnderline && (d->m_openElements.contains(Underline) || d->m_elementsToOpen.contains(Underline)) 0706 && !(d->m_openElements.contains(Anchor) || d->m_elementsToOpen.contains(Anchor))) { 0707 closedElements.insert(Underline); 0708 } 0709 0710 if (!fontItalic && (d->m_openElements.contains(Emph) || d->m_elementsToOpen.contains(Emph))) { 0711 closedElements.insert(Emph); 0712 } 0713 0714 if (fontWeight != QFont::Bold && (d->m_openElements.contains(Strong) || d->m_elementsToOpen.contains(Strong))) { 0715 closedElements.insert(Strong); 0716 } 0717 0718 if ((d->m_openElements.contains(SpanFontPointSize) || d->m_elementsToOpen.contains(SpanFontPointSize)) && (d->m_openFontPointSize != fontPointSize)) { 0719 closedElements.insert(SpanFontPointSize); 0720 } 0721 0722 if ((d->m_openElements.contains(SpanFontFamily) || d->m_elementsToOpen.contains(SpanFontFamily)) && (d->m_openFontFamily != fontFamily)) { 0723 closedElements.insert(SpanFontFamily); 0724 } 0725 0726 if ((d->m_openElements.contains(SpanBackground) && (d->m_openBackground != fontBackground)) 0727 || (d->m_elementsToOpen.contains(SpanBackground) && (d->m_backgroundToOpen != fontBackground))) { 0728 closedElements.insert(SpanBackground); 0729 } 0730 0731 if ((d->m_openElements.contains(SpanForeground) && (d->m_openForeground != fontForeground)) 0732 || (d->m_elementsToOpen.contains(SpanForeground) && (d->m_foregroundToOpen != fontForeground))) { 0733 closedElements.insert(SpanForeground); 0734 } 0735 0736 if ((d->m_openElements.contains(Anchor) && (d->m_openAnchorHref != anchorHref)) 0737 || (d->m_elementsToOpen.contains(Anchor) && (d->m_anchorHrefToOpen != anchorHref))) { 0738 closedElements.insert(Anchor); 0739 } 0740 0741 if (!subscript && (d->m_openElements.contains(SubScript) || d->m_elementsToOpen.contains(SubScript))) { 0742 closedElements.insert(SubScript); 0743 } 0744 0745 if (!superscript && (d->m_openElements.contains(SuperScript) || d->m_elementsToOpen.contains(SuperScript))) { 0746 closedElements.insert(SuperScript); 0747 } 0748 return closedElements; 0749 } 0750 0751 QList<int> MarkupDirector::getElementsToOpen(const QTextBlock::iterator &it) 0752 { 0753 Q_D(MarkupDirector); 0754 auto fragment = it.fragment(); 0755 if (!fragment.isValid()) { 0756 return {}; 0757 } 0758 const auto fragmentFormat = fragment.charFormat(); 0759 0760 const auto fontWeight = fragmentFormat.fontWeight(); 0761 const auto fontItalic = fragmentFormat.fontItalic(); 0762 const auto fontUnderline = fragmentFormat.fontUnderline(); 0763 const auto fontStrikeout = fragmentFormat.fontStrikeOut(); 0764 0765 const auto fontForeground = fragmentFormat.foreground(); 0766 const auto fontBackground = fragmentFormat.background(); 0767 0768 const QStringList families = fragmentFormat.fontFamilies().toStringList(); 0769 const auto fontFamily = families.isEmpty() ? QString() : families.constFirst(); 0770 const auto fontPointSize = fragmentFormat.font().pointSize(); 0771 const auto anchorHref = fragmentFormat.anchorHref(); 0772 0773 const auto vAlign = fragmentFormat.verticalAlignment(); 0774 const auto superscript = (vAlign == QTextCharFormat::AlignSuperScript); 0775 const auto subscript = (vAlign == QTextCharFormat::AlignSubScript); 0776 0777 if (superscript && !(d->m_openElements.contains(SuperScript))) { 0778 d->m_elementsToOpen.insert(SuperScript); 0779 } 0780 0781 if (subscript && !(d->m_openElements.contains(SubScript))) { 0782 d->m_elementsToOpen.insert(SubScript); 0783 } 0784 0785 if (!anchorHref.isEmpty() && !(d->m_openElements.contains(Anchor)) && (d->m_openAnchorHref != anchorHref)) { 0786 d->m_elementsToOpen.insert(Anchor); 0787 d->m_anchorHrefToOpen = anchorHref; 0788 } 0789 0790 if (fontForeground != Qt::NoBrush 0791 && !(d->m_openElements.contains(SpanForeground)) // Can only open one 0792 // foreground element 0793 // at a time. 0794 && (fontForeground != d->m_openForeground) 0795 && !((d->m_openElements.contains(Anchor) // Links can't have a foreground color. 0796 || d->m_elementsToOpen.contains(Anchor)))) { 0797 d->m_elementsToOpen.insert(SpanForeground); 0798 d->m_foregroundToOpen = fontForeground; 0799 } 0800 0801 if (fontBackground != Qt::NoBrush && !(d->m_openElements.contains(SpanBackground)) && (fontBackground != d->m_openBackground)) { 0802 d->m_elementsToOpen.insert(SpanBackground); 0803 d->m_backgroundToOpen = fontBackground; 0804 } 0805 0806 if (!fontFamily.isEmpty() && !(d->m_openElements.contains(SpanFontFamily)) && (fontFamily != d->m_openFontFamily)) { 0807 d->m_elementsToOpen.insert(SpanFontFamily); 0808 d->m_fontFamilyToOpen = fontFamily; 0809 } 0810 0811 if ((QTextCharFormat().font().pointSize() != fontPointSize) // Different from the default. 0812 && !(d->m_openElements.contains(SpanFontPointSize)) && (fontPointSize != d->m_openFontPointSize)) { 0813 d->m_elementsToOpen.insert(SpanFontPointSize); 0814 d->m_fontPointSizeToOpen = fontPointSize; 0815 } 0816 0817 // Only open a new bold tag if one is not already open. 0818 // eg, <b>some <i>mixed</i> format</b> should be as is, rather than 0819 // <b>some </b><b><i>mixed</i></b><b> format</b> 0820 0821 if (fontWeight >= QFont::DemiBold && !(d->m_openElements.contains(Strong))) { 0822 d->m_elementsToOpen.insert(Strong); 0823 } 0824 0825 if (fontItalic && !(d->m_openElements.contains(Emph))) { 0826 d->m_elementsToOpen.insert(Emph); 0827 } 0828 0829 if (fontUnderline && !(d->m_openElements.contains(Underline)) 0830 && !(d->m_openElements.contains(Anchor) || d->m_elementsToOpen.contains(Anchor)) // Can't change the underline state of a link. 0831 ) { 0832 d->m_elementsToOpen.insert(Underline); 0833 } 0834 0835 if (fontStrikeout && !(d->m_openElements.contains(StrikeOut))) { 0836 d->m_elementsToOpen.insert(StrikeOut); 0837 } 0838 0839 if (d->m_elementsToOpen.size() <= 1) { 0840 return d->m_elementsToOpen.values(); 0841 } 0842 return sortOpeningOrder(d->m_elementsToOpen, it); 0843 } 0844 0845 QList<int> MarkupDirector::sortOpeningOrder(QSet<int> openingOrder, QTextBlock::iterator it) const 0846 { 0847 QList<int> sortedOpenedElements; 0848 0849 // This is an insertion sort in a way. elements in openingOrder are assumed 0850 // to 0851 // be out of order. 0852 // The rest of the block is traversed until there are no more elements to 0853 // sort, or the end is reached. 0854 while (!openingOrder.isEmpty()) { 0855 if (!it.atEnd()) { 0856 it++; 0857 0858 if (!it.atEnd()) { 0859 // Because I've iterated, this returns the elements that will 0860 // be closed by the next fragment. 0861 const auto elementsToClose = getElementsToClose(it); 0862 0863 // The exact order these are opened in is irrelevant, as all 0864 // will be 0865 // closed on the same block. 0866 // See testDoubleFormat. 0867 for (int tag : elementsToClose) { 0868 if (openingOrder.remove(tag)) { 0869 sortedOpenedElements.prepend(tag); 0870 } 0871 } 0872 } 0873 } else { 0874 // End of block. Need to close all open elements. 0875 // Order irrelevant in this case. 0876 for (int tag : std::as_const(openingOrder)) { 0877 sortedOpenedElements.prepend(tag); 0878 } 0879 break; 0880 } 0881 } 0882 return sortedOpenedElements; 0883 }