KPimTextEdit

markupdirector.cpp
1/*
2 SPDX-FileCopyrightText: 2019-2020 Montel Laurent <montel@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5
6*/
7
8#include "markupdirector.h"
9#include "markupdirector_p.h"
10
11#include "abstractmarkupbuilder.h"
12
13#include <QBrush>
14#include <QColor>
15#include <QFlags>
16#include <QMap>
17#include <QStack>
18#include <QString>
19#include <QTextCharFormat>
20#include <QTextCursor>
21#include <QTextDocument>
22#include <QTextDocumentFragment>
23#include <QTextFrame>
24#include <QTextList>
25#include <QTextTable>
26
27#include <QDebug>
28using namespace KPIMTextEdit;
29
31 : d_ptr(new MarkupDirectorPrivate(this))
32 , m_builder(builder)
33{
34}
35
37{
38 delete d_ptr;
39}
40
41// #define ADD_HEADER_SUPPORT 1
42
44{
45 // Same code as grantlee but interpret margin
46
47 const auto blockFormat = block.blockFormat();
48 const auto blockAlignment = blockFormat.alignment();
49 const bool leftToRightText = block.textDirection() == Qt::RightToLeft;
50
51 // TODO: decide when to use <h1> etc.
52#ifdef ADD_HEADER_SUPPORT
53 if (blockFormat.headingLevel() > 0) {
54 // Header
55 qDebug() << " header " << blockFormat.headingLevel();
56 m_builder->beginHeader(blockFormat.headingLevel());
57 }
58#endif
59 if (blockFormat.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) {
61 if (!frameIt.atEnd()) {
62 return ++frameIt;
63 }
64 return frameIt;
65 }
66
67 auto it = block.begin();
68
69 // The beginning is the end. This is an empty block. Insert a newline and
70 // move
71 // on.
72 if (it.atEnd()) {
73 m_builder->addSingleBreakLine();
74
75 if (!frameIt.atEnd()) {
76 return ++frameIt;
77 }
78 return frameIt;
79 }
80
81 // Don't have p tags inside li tags.
82 if (!block.textList()) {
83 // Laurent : we need this margin as it's necessary to show blockquote
84
85 // Don't instruct builders to use margins. The rich text widget doesn't
86 // have
87 // an action for them yet,
88 // So users can't edit them. See bug
89 // https://bugs.kde.org/show_bug.cgi?id=160600
90 m_builder->beginParagraph(blockAlignment,
91 blockFormat.topMargin(),
92 blockFormat.bottomMargin(),
93 blockFormat.leftMargin(),
94 blockFormat.rightMargin(),
95 leftToRightText);
96 }
97
98 while (!it.atEnd()) {
99 it = processFragment(it, it.fragment(), block.document());
100 }
101 // Don't have p tags inside li tags.
102 if (!block.textList()) {
104 }
105
106 if (!frameIt.atEnd()) {
107 return ++frameIt;
108 }
109 return frameIt;
110}
111
113{
114 // Same code as Grantlee + a fix !
115
116 // Q_D( MarkupDirector );
117 const auto charFormat = fragment.charFormat();
118 // Need to check if it's a image format.
119 if (charFormat.isImageFormat()) {
120 const auto imageFormat = charFormat.toImageFormat();
121 return processImage(it, imageFormat, doc);
122 }
123
124 if (charFormat.objectType() >= QTextFormat::UserObject) {
125 processCustomFragment(fragment, doc);
126 if (!it.atEnd()) {
127 return ++it;
128 }
129 return it;
130 }
131
132 auto textObject = doc->objectForFormat(charFormat);
133 if (textObject) {
134 return processCharTextObject(it, fragment, textObject);
135 }
136
137 const auto textStr = fragment.text();
138 if (textStr.at(0).category() == QChar::Separator_Line) {
139 m_builder->addSingleBreakLine();
140 QString t;
141 for (int i = 1; i < textStr.length(); ++i) {
142 if (fragment.text().at(i).category() == QChar::Separator_Line) {
144 if (i < textStr.length() - 1) { // Don't add \n when we have the last char
145 m_builder->addSingleBreakLine();
146 }
147 t.clear();
148 } else {
149 t += fragment.text().at(i);
150 }
151 }
152 if (!t.isEmpty()) {
154 }
155
156 if (!it.atEnd()) {
157 return ++it;
158 }
159 return it;
160 }
161
162 // The order of closing and opening tags can determine whether generated
163 // html
164 // is valid or not.
165 // When processing a document with formatting which appears as
166 // '<b><i>Some</i>
167 // formatted<b> text',
168 // the correct generated output will contain '<strong><em>Some</em>
169 // formatted<strong> text'.
170 // However, processing text which appears as '<i><b>Some</b> formatted<i>
171 // text' might be incorrectly rendered
172 // as '<strong><em>Some</strong> formatted</em> text' if tags which start at
173 // the same fragment are
174 // opened out of order. Here, tags are not nested properly, and the html
175 // would
176 // not be valid or render correctly by unforgiving parsers (like QTextEdit).
177 // One solution is to make the order of opening tags dynamic. In the above
178 // case, the em tag would
179 // be opened before the strong tag '<em><strong>Some</strong> formatted</em>
180 // text'. That would
181 // require knowledge of which tag is going to close first. That might be
182 // possible by examining
183 // the 'next' QTextFragment while processing one.
184 //
185 // The other option is to do pessimistic closing of tags.
186 // In the above case, this means that if a fragment has two or more formats
187 // applied (bold and italic here),
188 // and one of them is closed, then all tags should be closed first. They
189 // will
190 // of course be reopened
191 // if necessary while processing the next fragment.
192 // The above case would be rendered as '<strong><em>Some</em></strong><em>
193 // formatted</em> text'.
194 //
195 // The first option is taken here, as the redundant opening and closing tags
196 // in the second option
197 // didn't appeal.
198 // See testDoubleStartDifferentFinish,
199 // testDoubleStartDifferentFinishReverseOrder
200
202
203 // If a sequence such as '<br /><br />' is imported into a document with
204 // setHtml, LineSeparator
205 // characters are inserted. Here I make sure to put them back.
206 auto sl = fragment.text().split(QChar(QChar::LineSeparator));
207 QStringListIterator i(sl);
208 auto paraClosed = false;
209 while (i.hasNext()) {
210 m_builder->appendLiteralText(i.next());
211 if (i.hasNext()) {
212 if (i.peekNext().isEmpty()) {
213 if (!paraClosed) {
215 paraClosed = true;
216 }
218 } else if (paraClosed) {
219 m_builder->beginParagraph(/* blockAlignment */);
220 paraClosed = false;
221 } else {
222 // Bug fixing : add missing single break line
223 m_builder->addSingleBreakLine();
224 }
225 }
226 }
227 if (!it.atEnd()) {
228 ++it;
229 }
230
232
233 return it;
234}
235
237{
238 while (!start.atEnd() && start != end) {
239 auto frame = start.currentFrame();
240 if (frame) {
241 auto table = qobject_cast<QTextTable *>(frame);
242 if (table) {
243 start = processTable(start, table);
244 } else {
245 start = processFrame(start, frame);
246 }
247 } else {
248 auto block = start.currentBlock();
249 Q_ASSERT(block.isValid());
250 start = processBlock(start, block);
251 }
252 }
253}
254
256{
257 if (frame) {
258 processDocumentContents(frame->begin(), frame->end());
259 }
260 if (!it.atEnd()) {
261 return ++it;
262 }
263 return it;
264}
265
267{
268 if (block.isValid()) {
269 auto fmt = block.blockFormat();
270 auto object = block.document()->objectForFormat(fmt);
271 if (object) {
272 return processObject(it, block, object);
273 } else {
274 return processBlockContents(it, block);
275 }
276 }
277
278 if (!it.atEnd()) {
279 return ++it;
280 }
281 return it;
282}
283
285{
286 const auto format = table->format();
287
288 const auto colLengths = format.columnWidthConstraints();
289
290 const auto tableWidth = format.width();
291 QString sWidth;
292
293 if (tableWidth.type() == QTextLength::PercentageLength) {
294 sWidth = QStringLiteral("%1%");
295 sWidth = sWidth.arg(tableWidth.rawValue());
296 } else if (tableWidth.type() == QTextLength::FixedLength) {
297 sWidth = QStringLiteral("%1");
298 sWidth = sWidth.arg(tableWidth.rawValue());
299 }
300
301 m_builder->beginTable(format.cellPadding(), format.cellSpacing(), sWidth);
302
303 const auto headerRowCount = format.headerRowCount();
304
305 QList<QTextTableCell> alreadyProcessedCells;
306
307 for (int row = 0, total = table->rows(); row < total; ++row) {
308 // Put a thead element around here somewhere?
309 // if (row < headerRowCount)
310 // {
311 // beginTableHeader();
312 // }
313
315
316 // Header attribute should really be on cells, not determined by number
317 // of
318 // rows.
319 // http://www.webdesignfromscratch.com/html-tables.cfm
320
321 for (int column = 0, total = table->columns(); column < total; ++column) {
322 auto tableCell = table->cellAt(row, column);
323
324 auto columnSpan = tableCell.columnSpan();
325 auto rowSpan = tableCell.rowSpan();
326 if ((rowSpan > 1) || (columnSpan > 1)) {
327 if (alreadyProcessedCells.contains(tableCell)) {
328 // Already processed this cell. Move on.
329 continue;
330 } else {
331 alreadyProcessedCells.append(tableCell);
332 }
333 }
334
335 auto cellWidth = colLengths.at(column);
336
337 QString sCellWidth;
338
339 if (cellWidth.type() == QTextLength::PercentageLength) {
340 sCellWidth = QStringLiteral("%1%").arg(cellWidth.rawValue());
341 } else if (cellWidth.type() == QTextLength::FixedLength) {
342 sCellWidth = QStringLiteral("%1").arg(cellWidth.rawValue());
343 }
344
345 // TODO: Use THEAD instead
346 if (row < headerRowCount) {
347 m_builder->beginTableHeaderCell(sCellWidth, columnSpan, rowSpan);
348 } else {
349 m_builder->beginTableCell(sCellWidth, columnSpan, rowSpan);
350 }
351
352 processTableCell(tableCell, table);
353
354 if (row < headerRowCount) {
356 } else {
358 }
359 }
361 }
363
364 if (!it.atEnd()) {
365 return ++it;
366 }
367 return it;
368}
369
371{
372 Q_UNUSED(table)
373 processDocumentContents(tableCell.begin(), tableCell.end());
374}
375
376QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::processList(QTextFrame::iterator it, const QTextBlock &_block, QTextList *list)
377{
378 const auto style = list->format().style();
379 m_builder->beginList(style);
380 auto block = _block;
381 auto curList = list;
382 while (block.isValid() && block.textList() && curList == list) {
384 processBlockContents(it, block);
386
387 if (!it.atEnd()) {
388 ++it;
389 }
390 block = block.next();
391 // Processing nested list
392 if (block.isValid() && block.textList() && block.textList()->format().indent() > list->format().indent()) {
393 auto obj = block.document()->objectForFormat(block.blockFormat());
394 auto group = qobject_cast<QTextBlockGroup *>(obj);
395 if (group && group != list) {
396 auto pair = processBlockGroup(it, block, group);
397 it = pair.first;
398 block = pair.second;
399 }
400 }
401 curList = qobject_cast<QTextList *>(block.document()->objectForFormat(block.blockFormat()));
402 }
404 return qMakePair(it, block);
405}
406
408{
409 Q_UNUSED(fragment)
410 Q_UNUSED(doc)
411}
412
414{
415 auto group = qobject_cast<QTextBlockGroup *>(object);
416 if (group) {
417 return processBlockGroup(it, block, group).first;
418 }
419 if (!it.atEnd()) {
420 return ++it;
421 }
422 return it;
423}
424
425QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::skipBlockGroup(QTextFrame::iterator it, const QTextBlock &_block, QTextBlockGroup *blockGroup)
426{
427 auto block = _block;
428 auto lastBlock = _block;
429 auto lastIt = it;
430 auto obj = block.document()->objectForFormat(block.blockFormat());
431 QTextBlockGroup *nextGroup;
432
433 if (!obj) {
434 return qMakePair(lastIt, lastBlock);
435 }
436
437 auto group = qobject_cast<QTextBlockGroup *>(obj);
438 if (!group) {
439 return qMakePair(lastIt, lastBlock);
440 }
441
442 while (block.isValid()) {
443 if (!group) {
444 break;
445 }
446
447 block = block.next();
448 if (!it.atEnd()) {
449 ++it;
450 }
451
452 obj = block.document()->objectForFormat(block.blockFormat());
453 if (obj) {
454 continue;
455 }
456
457 nextGroup = qobject_cast<QTextBlockGroup *>(obj);
458
459 if (group == blockGroup || !nextGroup) {
460 lastBlock = block;
461 lastIt = it;
462 }
463 group = nextGroup;
464 }
465 return qMakePair(lastIt, lastBlock);
466}
467
468QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::processBlockGroup(const QTextFrame::iterator &it, const QTextBlock &block, QTextBlockGroup *blockGroup)
469{
470 const auto list = qobject_cast<QTextList *>(blockGroup);
471 if (list) {
472 return processList(it, block, list);
473 }
474 return skipBlockGroup(it, block, blockGroup);
475}
476
481
483{
484 const auto fragmentFormat = fragment.charFormat();
485 if (fragmentFormat.isImageFormat()) {
486 const auto imageFormat = fragmentFormat.toImageFormat();
487 return processImage(it, imageFormat, textObject->document());
488 }
489 if (!it.atEnd()) {
490 return ++it;
491 }
492 return it;
493}
494
496{
497 Q_UNUSED(doc)
498 // TODO: Close any open format elements?
499 m_builder->insertImage(imageFormat.name(), imageFormat.width(), imageFormat.height());
500 if (!it.atEnd()) {
501 return ++it;
502 }
503 return it;
504}
505
507{
509 // The order of closing elements is determined by the order they were opened
510 // in.
511 // The order of opened elements is in the openElements member list.
512 // see testDifferentStartDoubleFinish and
513 // testDifferentStartDoubleFinishReverseOrder
514
515 if (d->m_openElements.isEmpty()) {
516 return;
517 }
518
519 auto elementsToClose = getElementsToClose(it);
520
521 int previousSize;
522 auto remainingSize = elementsToClose.size();
523 while (!elementsToClose.isEmpty()) {
524 auto tag = d->m_openElements.last();
525 if (elementsToClose.contains(tag)) {
526 switch (tag) {
527 case Strong:
529 break;
530 case Emph:
532 break;
533 case Underline:
535 break;
536 case StrikeOut:
538 break;
541 break;
542 case SpanFontFamily:
544 break;
545 case SpanBackground:
547 // Clear background otherwise if we select 2 lines with same color it will not applied. bug #442416
548 d->m_openBackground = {};
549 break;
550 case SpanForeground:
552 // Clear foreground text color otherwise if we select 2 lines with same text color it will not applied. bug #442416
553 d->m_openForeground = {};
554 break;
555 case Anchor:
557 break;
558 case SubScript:
560 break;
561 case SuperScript:
563 break;
564
565 default:
566 break;
567 }
568 d->m_openElements.removeLast();
569 elementsToClose.remove(tag);
570 }
571 previousSize = remainingSize;
572 remainingSize = elementsToClose.size();
573
574 if (previousSize == remainingSize) {
575 // Iterated once through without closing any tags.
576 // This means that there's overlap in the tags, such as
577 // 'text with <b>some <i>formatting</i></b><i> tags</i>'
578 // See testOverlap.
579 // The top element in openElements must be a blocker, so close it on
580 // next
581 // iteration.
582 elementsToClose.insert(d->m_openElements.last());
583 }
584 }
585}
586
588{
590 auto fragment = it.fragment();
591
592 if (!fragment.isValid()) {
593 return;
594 }
595
596 const auto fragmentFormat = fragment.charFormat();
597 const auto elementsToOpenList = getElementsToOpen(it);
598
599 for (int tag : elementsToOpenList) {
600 switch (tag) {
601 case Strong:
603 break;
604 case Emph:
606 break;
607 case Underline:
609 break;
610 case StrikeOut:
612 break;
614 d->m_openFontPointSize = fragmentFormat.font().pointSize();
615 m_builder->beginFontPointSize(d->m_openFontPointSize);
616 break;
617 case SpanFontFamily:
618 d->m_openFontFamily =
619 fragmentFormat.fontFamilies().toStringList().isEmpty() ? QString() : fragmentFormat.fontFamilies().toStringList().constFirst();
620 m_builder->beginFontFamily(d->m_openFontFamily);
621 break;
622 case SpanBackground:
623 d->m_openBackground = fragmentFormat.background();
624 m_builder->beginBackground(d->m_openBackground);
625 break;
626 case SpanForeground:
627 d->m_openForeground = fragmentFormat.foreground();
628 m_builder->beginForeground(d->m_openForeground);
629 break;
630 case Anchor: {
631 // TODO: Multiple anchor names here.
632 auto anchorNames = fragmentFormat.anchorNames();
633 if (!anchorNames.isEmpty()) {
634 while (!anchorNames.isEmpty()) {
635 auto n = anchorNames.last();
636 anchorNames.removeLast();
637 if (anchorNames.isEmpty()) {
638 // Doesn't matter if anchorHref is empty.
639 m_builder->beginAnchor(fragmentFormat.anchorHref(), n);
640 break;
641 } else {
642 // Empty <a> tags allow multiple names for the same
643 // section.
646 }
647 }
648 } else {
649 m_builder->beginAnchor(fragmentFormat.anchorHref());
650 }
651 d->m_openAnchorHref = fragmentFormat.anchorHref();
652 break;
653 }
654 case SuperScript:
656 break;
657 case SubScript:
659 break;
660 default:
661 break;
662 }
663 d->m_openElements.append(tag);
664 d->m_elementsToOpen.remove(tag);
665 }
666}
667
669{
670 Q_D(const MarkupDirector);
671 QSet<int> closedElements;
672
673 if (it.atEnd()) {
674 // End of block?. Close all open tags.
675
676 const QList<int> openElement = d->m_openElements;
677 auto elementsToClose = QSet<int>(openElement.begin(), openElement.end());
678 return elementsToClose.unite(d->m_elementsToOpen);
679 }
680
681 auto fragment = it.fragment();
682
683 if (!fragment.isValid()) {
684 return closedElements;
685 }
686
687 const auto fragmentFormat = fragment.charFormat();
688 const auto fontWeight = fragmentFormat.fontWeight();
689 const auto fontItalic = fragmentFormat.fontItalic();
690 const auto fontUnderline = fragmentFormat.fontUnderline();
691 const auto fontStrikeout = fragmentFormat.fontStrikeOut();
692
693 const auto fontForeground = fragmentFormat.foreground();
694 const auto fontBackground = fragmentFormat.background();
695 const QStringList families = fragmentFormat.fontFamilies().toStringList();
696 const auto fontFamily = families.isEmpty() ? QString() : families.constFirst();
697 const auto fontPointSize = fragmentFormat.font().pointSize();
698 const auto anchorHref = fragmentFormat.anchorHref();
699
700 const auto vAlign = fragmentFormat.verticalAlignment();
701 const auto superscript = (vAlign == QTextCharFormat::AlignSuperScript);
702 const auto subscript = (vAlign == QTextCharFormat::AlignSubScript);
703
704 if (!fontStrikeout && (d->m_openElements.contains(StrikeOut) || d->m_elementsToOpen.contains(StrikeOut))) {
705 closedElements.insert(StrikeOut);
706 }
707
708 if (!fontUnderline && (d->m_openElements.contains(Underline) || d->m_elementsToOpen.contains(Underline))
709 && !(d->m_openElements.contains(Anchor) || d->m_elementsToOpen.contains(Anchor))) {
710 closedElements.insert(Underline);
711 }
712
713 if (!fontItalic && (d->m_openElements.contains(Emph) || d->m_elementsToOpen.contains(Emph))) {
714 closedElements.insert(Emph);
715 }
716
717 if (fontWeight != QFont::Bold && (d->m_openElements.contains(Strong) || d->m_elementsToOpen.contains(Strong))) {
718 closedElements.insert(Strong);
719 }
720
721 if ((d->m_openElements.contains(SpanFontPointSize) || d->m_elementsToOpen.contains(SpanFontPointSize)) && (d->m_openFontPointSize != fontPointSize)) {
722 closedElements.insert(SpanFontPointSize);
723 }
724
725 if ((d->m_openElements.contains(SpanFontFamily) || d->m_elementsToOpen.contains(SpanFontFamily)) && (d->m_openFontFamily != fontFamily)) {
726 closedElements.insert(SpanFontFamily);
727 }
728
729 if ((d->m_openElements.contains(SpanBackground) && (d->m_openBackground != fontBackground))
730 || (d->m_elementsToOpen.contains(SpanBackground) && (d->m_backgroundToOpen != fontBackground))) {
731 closedElements.insert(SpanBackground);
732 }
733
734 if ((d->m_openElements.contains(SpanForeground) && (d->m_openForeground != fontForeground))
735 || (d->m_elementsToOpen.contains(SpanForeground) && (d->m_foregroundToOpen != fontForeground))) {
736 closedElements.insert(SpanForeground);
737 }
738
739 if ((d->m_openElements.contains(Anchor) && (d->m_openAnchorHref != anchorHref))
740 || (d->m_elementsToOpen.contains(Anchor) && (d->m_anchorHrefToOpen != anchorHref))) {
741 closedElements.insert(Anchor);
742 }
743
744 if (!subscript && (d->m_openElements.contains(SubScript) || d->m_elementsToOpen.contains(SubScript))) {
745 closedElements.insert(SubScript);
746 }
747
748 if (!superscript && (d->m_openElements.contains(SuperScript) || d->m_elementsToOpen.contains(SuperScript))) {
749 closedElements.insert(SuperScript);
750 }
751 return closedElements;
752}
753
755{
757 auto fragment = it.fragment();
758 if (!fragment.isValid()) {
759 return {};
760 }
761 const auto fragmentFormat = fragment.charFormat();
762
763 const auto fontWeight = fragmentFormat.fontWeight();
764 const auto fontItalic = fragmentFormat.fontItalic();
765 const auto fontUnderline = fragmentFormat.fontUnderline();
766 const auto fontStrikeout = fragmentFormat.fontStrikeOut();
767
768 const auto fontForeground = fragmentFormat.foreground();
769 const auto fontBackground = fragmentFormat.background();
770
771 const QStringList families = fragmentFormat.fontFamilies().toStringList();
772 const auto fontFamily = families.isEmpty() ? QString() : families.constFirst();
773 const auto fontPointSize = fragmentFormat.font().pointSize();
774 const auto anchorHref = fragmentFormat.anchorHref();
775
776 const auto vAlign = fragmentFormat.verticalAlignment();
777 const auto superscript = (vAlign == QTextCharFormat::AlignSuperScript);
778 const auto subscript = (vAlign == QTextCharFormat::AlignSubScript);
779
780 if (superscript && !(d->m_openElements.contains(SuperScript))) {
781 d->m_elementsToOpen.insert(SuperScript);
782 }
783
784 if (subscript && !(d->m_openElements.contains(SubScript))) {
785 d->m_elementsToOpen.insert(SubScript);
786 }
787
788 if (!anchorHref.isEmpty() && !(d->m_openElements.contains(Anchor)) && (d->m_openAnchorHref != anchorHref)) {
789 d->m_elementsToOpen.insert(Anchor);
790 d->m_anchorHrefToOpen = anchorHref;
791 }
792
793 if (fontForeground != Qt::NoBrush
794 && !(d->m_openElements.contains(SpanForeground)) // Can only open one
795 // foreground element
796 // at a time.
797 && (fontForeground != d->m_openForeground)
798 && !((d->m_openElements.contains(Anchor) // Links can't have a foreground color.
799 || d->m_elementsToOpen.contains(Anchor)))) {
800 d->m_elementsToOpen.insert(SpanForeground);
801 d->m_foregroundToOpen = fontForeground;
802 }
803
804 if (fontBackground != Qt::NoBrush && !(d->m_openElements.contains(SpanBackground)) && (fontBackground != d->m_openBackground)) {
805 d->m_elementsToOpen.insert(SpanBackground);
806 d->m_backgroundToOpen = fontBackground;
807 }
808
809 if (!fontFamily.isEmpty() && !(d->m_openElements.contains(SpanFontFamily)) && (fontFamily != d->m_openFontFamily)) {
810 d->m_elementsToOpen.insert(SpanFontFamily);
811 d->m_fontFamilyToOpen = fontFamily;
812 }
813
814 if ((QTextCharFormat().font().pointSize() != fontPointSize) // Different from the default.
815 && !(d->m_openElements.contains(SpanFontPointSize)) && (fontPointSize != d->m_openFontPointSize)) {
816 d->m_elementsToOpen.insert(SpanFontPointSize);
817 d->m_fontPointSizeToOpen = fontPointSize;
818 }
819
820 // Only open a new bold tag if one is not already open.
821 // eg, <b>some <i>mixed</i> format</b> should be as is, rather than
822 // <b>some </b><b><i>mixed</i></b><b> format</b>
823
824 if (fontWeight >= QFont::DemiBold && !(d->m_openElements.contains(Strong))) {
825 d->m_elementsToOpen.insert(Strong);
826 }
827
828 if (fontItalic && !(d->m_openElements.contains(Emph))) {
829 d->m_elementsToOpen.insert(Emph);
830 }
831
832 if (fontUnderline && !(d->m_openElements.contains(Underline))
833 && !(d->m_openElements.contains(Anchor) || d->m_elementsToOpen.contains(Anchor)) // Can't change the underline state of a link.
834 ) {
835 d->m_elementsToOpen.insert(Underline);
836 }
837
838 if (fontStrikeout && !(d->m_openElements.contains(StrikeOut))) {
839 d->m_elementsToOpen.insert(StrikeOut);
840 }
841
842 if (d->m_elementsToOpen.size() <= 1) {
843 return d->m_elementsToOpen.values();
844 }
845 return sortOpeningOrder(d->m_elementsToOpen, it);
846}
847
849{
850 QList<int> sortedOpenedElements;
851
852 // This is an insertion sort in a way. elements in openingOrder are assumed
853 // to
854 // be out of order.
855 // The rest of the block is traversed until there are no more elements to
856 // sort, or the end is reached.
857 while (!openingOrder.isEmpty()) {
858 if (!it.atEnd()) {
859 it++;
860
861 if (!it.atEnd()) {
862 // Because I've iterated, this returns the elements that will
863 // be closed by the next fragment.
864 const auto elementsToClose = getElementsToClose(it);
865
866 // The exact order these are opened in is irrelevant, as all
867 // will be
868 // closed on the same block.
869 // See testDoubleFormat.
870 for (int tag : elementsToClose) {
871 if (openingOrder.remove(tag)) {
872 sortedOpenedElements.prepend(tag);
873 }
874 }
875 }
876 } else {
877 // End of block. Need to close all open elements.
878 // Order irrelevant in this case.
879 for (int tag : std::as_const(openingOrder)) {
880 sortedOpenedElements.prepend(tag);
881 }
882 break;
883 }
884 }
885 return sortedOpenedElements;
886}
Interface for creating marked-up text output.
virtual void beginFontPointSize(int size)=0
Begin a new font point size element in the markup.
virtual void beginSubscript()=0
Begin a subscript element.
virtual void beginList(QTextListFormat::Style style)=0
Begin a new list element in the markup.
virtual void endTableHeaderCell()=0
End a table header cell.
virtual void endParagraph()=0
Close the paragraph in the markup.
virtual void endSuperscript()=0
End superscript element.
virtual void beginEmph()=0
Begin an emphasised element in the markup.
virtual void endForeground()=0
Close the decorarated foreground element in the markup.
virtual void beginUnderline()=0
Begin an underlined element in the markup.
virtual void beginSuperscript()=0
Begin a superscript element.
virtual void beginBackground(const QBrush &brush)=0
Begin a decorarated background element in the markup (A text background color) using brush.
virtual void beginStrikeout()=0
Begin a struck out element in the markup.
virtual void beginTableCell(const QString &width, int colSpan, int rowSpan)=0
Begin a new table cell.
virtual void endTable()=0
End a table element.
virtual void beginParagraph(Qt::Alignment a=Qt::AlignLeft, qreal top=0.0, qreal bottom=0.0, qreal left=0.0, qreal right=0.0, bool leftToRightText=false)=0
Begin a new paragraph in the markup.
virtual void endTableRow()=0
End a table row.
virtual void endStrong()=0
Close the bold element in the markup.
virtual void appendLiteralText(const QString &text)=0
Append the plain text text to the markup.
virtual void endList()=0
Close the list.
virtual void endBackground()=0
Close the decorarated background element in the markup.
virtual void insertHorizontalRule(int width=-1)=0
Insert a horizontal rule into the markup.
virtual void endFontFamily()=0
End font family element.
virtual void beginTable(qreal cellpadding, qreal cellspacing, const QString &width)=0
Begin a table element.
virtual void endAnchor()=0
Close the anchor element.
virtual void endSubscript()=0
End subscript element.
virtual void beginStrong()=0
Begin a bold element in the markup.
virtual void beginForeground(const QBrush &brush)=0
Begin a decorarated foreground element in the markup (A text color) using brush.
virtual void insertImage(const QString &url, qreal width, qreal height)=0
Insert a new image element into the markup.
virtual void endFontPointSize()=0
End font point size element.
virtual void endEmph()=0
Close the emphasised element in the markup.
virtual void beginHeader(int level)=0
Begin a level level header.
virtual void beginFontFamily(const QString &family)=0
Begin a new font family element in the markup.
virtual void endListItem()=0
End the list item.
virtual void endTableCell()=0
End a table cell.
virtual void beginTableHeaderCell(const QString &width, int colSpan, int rowSpan)=0
Begin a new table header cell.
virtual void endStrikeout()=0
Close the struck out element in the markup.
virtual void beginAnchor(const QString &href={}, const QString &name={})=0
Begin a url anchor element in the markup.
virtual void beginTableRow()=0
Begin a new table row.
virtual void addNewline()=0
Add a newline to the markup.
virtual void endUnderline()=0
Close the underlined element in the markup.
virtual void beginListItem()=0
Begin a new list item in the markup.
Instructs a builder object to create markup output.
virtual QTextFrame::iterator processBlock(QTextFrame::iterator it, const QTextBlock &block)
Directs the builder to create output for the single block.
@ StrikeOut
An underline tag is open.
@ SpanFontFamily
A background altering span tag is open.
@ SubScript
A superscript tag is open.
@ SuperScript
No tags are open.
@ Anchor
A subscript tag is open.
@ Strong
A font size altering span tag is open.
@ SpanBackground
A foreground altering span tag is open.
@ SpanForeground
An anchor tag is open.
@ Underline
A emphasis tag is open.
@ Emph
A strong tag is open.
@ SpanFontPointSize
A font family altering span tag is open.
virtual void processTableCell(const QTextTableCell &tableCell, QTextTable *table)
Directs the builder to create output for the contents of the single tableCell.
virtual QSet< int > getElementsToClose(const QTextBlock::iterator &it) const
Returns the tags that should be closed at the position of it.
KPIMTextEdit::AbstractMarkupBuilder *const m_builder
The builder this MarkupDirector is operating on.
virtual QTextBlock::iterator processCharTextObject(QTextBlock::iterator it, const QTextFragment &fragment, QTextObject *textObject)
Directs the builder to create output for the contents of the single textObject.
virtual QTextFrame::iterator processTable(QTextFrame::iterator it, QTextTable *table)
Directs the builder to create output for the contents of the single table.
MarkupDirector(KPIMTextEdit::AbstractMarkupBuilder *builder)
Constructor.
virtual void processCustomFragment(const QTextFragment &fragment, QTextDocument const *doc)
Hook for instructing the builder to create output for the fragemnt with a custom type.
virtual void processOpeningElements(const QTextBlock::iterator &it)
Directs the builder to open the appropriate tags at the position of it.
virtual void processDocument(QTextDocument *doc)
Constructs the output by directing the builder to create the markup.
virtual QPair< QTextFrame::iterator, QTextBlock > processBlockGroup(const QTextFrame::iterator &it, const QTextBlock &block, QTextBlockGroup *textBlockGroup)
Directs the builder to create output for the single textBlockGroup.
virtual QTextFrame::iterator processFrame(QTextFrame::iterator it, QTextFrame *frame)
Directs the builder to create output for the single frame.
void processDocumentContents(QTextFrame::iterator begin, const QTextFrame::iterator &end)
Processes the document between begin and end.
virtual QPair< QTextFrame::iterator, QTextBlock > processList(QTextFrame::iterator it, const QTextBlock &block, QTextList *textList)
Directs the builder to create output for the single textList.
QList< int > sortOpeningOrder(QSet< int > openingTags, QTextBlock::iterator it) const
Returns a list of tags contained in openingTags sorted so they can be opened in order and will be clo...
virtual QList< int > getElementsToOpen(const QTextBlock::iterator &it)
Returns the tags that should be opened at the position of it.
virtual QTextBlock::iterator processImage(QTextBlock::iterator it, const QTextImageFormat &imageFormat, QTextDocument const *doc)
Directs the builder to create output for the image represented by the imageFormat.
QPair< QTextFrame::iterator, QTextBlock > skipBlockGroup(QTextFrame::iterator it, const QTextBlock &_block, QTextBlockGroup *blockGroup)
Iterates the iterator it to the first block after blockGroup.
virtual QTextFrame::iterator processObject(QTextFrame::iterator it, const QTextBlock &block, QTextObject *textObject)
Directs the builder to create output for the single textObject.
virtual QTextFrame::iterator processBlockContents(QTextFrame::iterator it, const QTextBlock &block)
Directs the builder to create output for the contents of the single block.
virtual QTextBlock::iterator processFragment(QTextBlock::iterator it, const QTextFragment &fragment, QTextDocument const *doc)
Directs the builder to create output for the contents of the single fragment.
virtual void processClosingElements(const QTextBlock::iterator &it)
Directs the builder to close the appropriate tags at the position of it.
virtual ~MarkupDirector()
Destructor.
Q_SCRIPTABLE Q_NOREPLY void start()
Separator_Line
Category category(char32_t ucs4)
void append(QList< T > &&value)
iterator begin()
const T & constFirst() const const
bool contains(const AT &value) const const
iterator end()
bool isEmpty() const const
void prepend(parameter_type value)
iterator insert(const T &value)
bool isEmpty() const const
bool remove(const T &value)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void clear()
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
RightToLeft
iterator begin() const const
QTextBlockFormat blockFormat() const const
const QTextDocument * document() const const
bool isValid() const const
Qt::LayoutDirection textDirection() const const
QTextList * textList() const const
Qt::Alignment alignment() const const
QTextObject * objectForFormat(const QTextFormat &f) const const
QTextFrame * rootFrame() const const
BlockTrailingHorizontalRulerWidth
QTextCharFormat charFormat() const const
QString text() const const
iterator begin() const const
iterator end() const const
qreal height() const const
QString name() const const
qreal width() const const
QTextDocument * document() const const
QTextTableCell cellAt(const QTextCursor &cursor) const const
int columns() const const
QTextTableFormat format() const const
int rows() const const
QTextFrame::iterator begin() const const
int columnSpan() const const
QTextFrame::iterator end() const const
QList< QTextLength > columnWidthConstraints() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:19:16 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.