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 while (block.isValid() && block.textList()) {
383 processBlockContents(it, block);
385
386 if (!it.atEnd()) {
387 ++it;
388 }
389 block = block.next();
390 if (block.isValid()) {
391 auto obj = block.document()->objectForFormat(block.blockFormat());
392 auto group = qobject_cast<QTextBlockGroup *>(obj);
393 if (group && group != list) {
394 auto pair = processBlockGroup(it, block, group);
395 it = pair.first;
396 block = pair.second;
397 }
398 }
399 }
401 return qMakePair(it, block);
402}
403
405{
406 Q_UNUSED(fragment)
407 Q_UNUSED(doc)
408}
409
411{
412 auto group = qobject_cast<QTextBlockGroup *>(object);
413 if (group) {
414 return processBlockGroup(it, block, group).first;
415 }
416 if (!it.atEnd()) {
417 return ++it;
418 }
419 return it;
420}
421
422QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::skipBlockGroup(QTextFrame::iterator it, const QTextBlock &_block, QTextBlockGroup *blockGroup)
423{
424 auto block = _block;
425 auto lastBlock = _block;
426 auto lastIt = it;
427 auto obj = block.document()->objectForFormat(block.blockFormat());
428 QTextBlockGroup *nextGroup;
429
430 if (!obj) {
431 return qMakePair(lastIt, lastBlock);
432 }
433
434 auto group = qobject_cast<QTextBlockGroup *>(obj);
435 if (!group) {
436 return qMakePair(lastIt, lastBlock);
437 }
438
439 while (block.isValid()) {
440 if (!group) {
441 break;
442 }
443
444 block = block.next();
445 if (!it.atEnd()) {
446 ++it;
447 }
448
449 obj = block.document()->objectForFormat(block.blockFormat());
450 if (obj) {
451 continue;
452 }
453
454 nextGroup = qobject_cast<QTextBlockGroup *>(obj);
455
456 if (group == blockGroup || !nextGroup) {
457 lastBlock = block;
458 lastIt = it;
459 }
460 group = nextGroup;
461 }
462 return qMakePair(lastIt, lastBlock);
463}
464
465QPair<QTextFrame::iterator, QTextBlock> MarkupDirector::processBlockGroup(const QTextFrame::iterator &it, const QTextBlock &block, QTextBlockGroup *blockGroup)
466{
467 const auto list = qobject_cast<QTextList *>(blockGroup);
468 if (list) {
469 return processList(it, block, list);
470 }
471 return skipBlockGroup(it, block, blockGroup);
472}
473
478
480{
481 const auto fragmentFormat = fragment.charFormat();
482 if (fragmentFormat.isImageFormat()) {
483 const auto imageFormat = fragmentFormat.toImageFormat();
484 return processImage(it, imageFormat, textObject->document());
485 }
486 if (!it.atEnd()) {
487 return ++it;
488 }
489 return it;
490}
491
493{
494 Q_UNUSED(doc)
495 // TODO: Close any open format elements?
496 m_builder->insertImage(imageFormat.name(), imageFormat.width(), imageFormat.height());
497 if (!it.atEnd()) {
498 return ++it;
499 }
500 return it;
501}
502
504{
506 // The order of closing elements is determined by the order they were opened
507 // in.
508 // The order of opened elements is in the openElements member list.
509 // see testDifferentStartDoubleFinish and
510 // testDifferentStartDoubleFinishReverseOrder
511
512 if (d->m_openElements.isEmpty()) {
513 return;
514 }
515
516 auto elementsToClose = getElementsToClose(it);
517
518 int previousSize;
519 auto remainingSize = elementsToClose.size();
520 while (!elementsToClose.isEmpty()) {
521 auto tag = d->m_openElements.last();
522 if (elementsToClose.contains(tag)) {
523 switch (tag) {
524 case Strong:
526 break;
527 case Emph:
529 break;
530 case Underline:
532 break;
533 case StrikeOut:
535 break;
538 break;
539 case SpanFontFamily:
541 break;
542 case SpanBackground:
544 // Clear background otherwise if we select 2 lines with same color it will not applied. bug #442416
545 d->m_openBackground = {};
546 break;
547 case SpanForeground:
549 // Clear foreground text color otherwise if we select 2 lines with same text color it will not applied. bug #442416
550 d->m_openForeground = {};
551 break;
552 case Anchor:
554 break;
555 case SubScript:
557 break;
558 case SuperScript:
560 break;
561
562 default:
563 break;
564 }
565 d->m_openElements.removeLast();
566 elementsToClose.remove(tag);
567 }
568 previousSize = remainingSize;
569 remainingSize = elementsToClose.size();
570
571 if (previousSize == remainingSize) {
572 // Iterated once through without closing any tags.
573 // This means that there's overlap in the tags, such as
574 // 'text with <b>some <i>formatting</i></b><i> tags</i>'
575 // See testOverlap.
576 // The top element in openElements must be a blocker, so close it on
577 // next
578 // iteration.
579 elementsToClose.insert(d->m_openElements.last());
580 }
581 }
582}
583
585{
587 auto fragment = it.fragment();
588
589 if (!fragment.isValid()) {
590 return;
591 }
592
593 const auto fragmentFormat = fragment.charFormat();
594 const auto elementsToOpenList = getElementsToOpen(it);
595
596 for (int tag : elementsToOpenList) {
597 switch (tag) {
598 case Strong:
600 break;
601 case Emph:
603 break;
604 case Underline:
606 break;
607 case StrikeOut:
609 break;
611 d->m_openFontPointSize = fragmentFormat.font().pointSize();
612 m_builder->beginFontPointSize(d->m_openFontPointSize);
613 break;
614 case SpanFontFamily:
615 d->m_openFontFamily =
616 fragmentFormat.fontFamilies().toStringList().isEmpty() ? QString() : fragmentFormat.fontFamilies().toStringList().constFirst();
617 m_builder->beginFontFamily(d->m_openFontFamily);
618 break;
619 case SpanBackground:
620 d->m_openBackground = fragmentFormat.background();
621 m_builder->beginBackground(d->m_openBackground);
622 break;
623 case SpanForeground:
624 d->m_openForeground = fragmentFormat.foreground();
625 m_builder->beginForeground(d->m_openForeground);
626 break;
627 case Anchor: {
628 // TODO: Multiple anchor names here.
629 auto anchorNames = fragmentFormat.anchorNames();
630 if (!anchorNames.isEmpty()) {
631 while (!anchorNames.isEmpty()) {
632 auto n = anchorNames.last();
633 anchorNames.removeLast();
634 if (anchorNames.isEmpty()) {
635 // Doesn't matter if anchorHref is empty.
636 m_builder->beginAnchor(fragmentFormat.anchorHref(), n);
637 break;
638 } else {
639 // Empty <a> tags allow multiple names for the same
640 // section.
643 }
644 }
645 } else {
646 m_builder->beginAnchor(fragmentFormat.anchorHref());
647 }
648 d->m_openAnchorHref = fragmentFormat.anchorHref();
649 break;
650 }
651 case SuperScript:
653 break;
654 case SubScript:
656 break;
657 default:
658 break;
659 }
660 d->m_openElements.append(tag);
661 d->m_elementsToOpen.remove(tag);
662 }
663}
664
666{
667 Q_D(const MarkupDirector);
668 QSet<int> closedElements;
669
670 if (it.atEnd()) {
671 // End of block?. Close all open tags.
672
673 const QList<int> openElement = d->m_openElements;
674 auto elementsToClose = QSet<int>(openElement.begin(), openElement.end());
675 return elementsToClose.unite(d->m_elementsToOpen);
676 }
677
678 auto fragment = it.fragment();
679
680 if (!fragment.isValid()) {
681 return closedElements;
682 }
683
684 const auto fragmentFormat = fragment.charFormat();
685 const auto fontWeight = fragmentFormat.fontWeight();
686 const auto fontItalic = fragmentFormat.fontItalic();
687 const auto fontUnderline = fragmentFormat.fontUnderline();
688 const auto fontStrikeout = fragmentFormat.fontStrikeOut();
689
690 const auto fontForeground = fragmentFormat.foreground();
691 const auto fontBackground = fragmentFormat.background();
692 const QStringList families = fragmentFormat.fontFamilies().toStringList();
693 const auto fontFamily = families.isEmpty() ? QString() : families.constFirst();
694 const auto fontPointSize = fragmentFormat.font().pointSize();
695 const auto anchorHref = fragmentFormat.anchorHref();
696
697 const auto vAlign = fragmentFormat.verticalAlignment();
698 const auto superscript = (vAlign == QTextCharFormat::AlignSuperScript);
699 const auto subscript = (vAlign == QTextCharFormat::AlignSubScript);
700
701 if (!fontStrikeout && (d->m_openElements.contains(StrikeOut) || d->m_elementsToOpen.contains(StrikeOut))) {
702 closedElements.insert(StrikeOut);
703 }
704
705 if (!fontUnderline && (d->m_openElements.contains(Underline) || d->m_elementsToOpen.contains(Underline))
706 && !(d->m_openElements.contains(Anchor) || d->m_elementsToOpen.contains(Anchor))) {
707 closedElements.insert(Underline);
708 }
709
710 if (!fontItalic && (d->m_openElements.contains(Emph) || d->m_elementsToOpen.contains(Emph))) {
711 closedElements.insert(Emph);
712 }
713
714 if (fontWeight != QFont::Bold && (d->m_openElements.contains(Strong) || d->m_elementsToOpen.contains(Strong))) {
715 closedElements.insert(Strong);
716 }
717
718 if ((d->m_openElements.contains(SpanFontPointSize) || d->m_elementsToOpen.contains(SpanFontPointSize)) && (d->m_openFontPointSize != fontPointSize)) {
719 closedElements.insert(SpanFontPointSize);
720 }
721
722 if ((d->m_openElements.contains(SpanFontFamily) || d->m_elementsToOpen.contains(SpanFontFamily)) && (d->m_openFontFamily != fontFamily)) {
723 closedElements.insert(SpanFontFamily);
724 }
725
726 if ((d->m_openElements.contains(SpanBackground) && (d->m_openBackground != fontBackground))
727 || (d->m_elementsToOpen.contains(SpanBackground) && (d->m_backgroundToOpen != fontBackground))) {
728 closedElements.insert(SpanBackground);
729 }
730
731 if ((d->m_openElements.contains(SpanForeground) && (d->m_openForeground != fontForeground))
732 || (d->m_elementsToOpen.contains(SpanForeground) && (d->m_foregroundToOpen != fontForeground))) {
733 closedElements.insert(SpanForeground);
734 }
735
736 if ((d->m_openElements.contains(Anchor) && (d->m_openAnchorHref != anchorHref))
737 || (d->m_elementsToOpen.contains(Anchor) && (d->m_anchorHrefToOpen != anchorHref))) {
738 closedElements.insert(Anchor);
739 }
740
741 if (!subscript && (d->m_openElements.contains(SubScript) || d->m_elementsToOpen.contains(SubScript))) {
742 closedElements.insert(SubScript);
743 }
744
745 if (!superscript && (d->m_openElements.contains(SuperScript) || d->m_elementsToOpen.contains(SuperScript))) {
746 closedElements.insert(SuperScript);
747 }
748 return closedElements;
749}
750
752{
754 auto fragment = it.fragment();
755 if (!fragment.isValid()) {
756 return {};
757 }
758 const auto fragmentFormat = fragment.charFormat();
759
760 const auto fontWeight = fragmentFormat.fontWeight();
761 const auto fontItalic = fragmentFormat.fontItalic();
762 const auto fontUnderline = fragmentFormat.fontUnderline();
763 const auto fontStrikeout = fragmentFormat.fontStrikeOut();
764
765 const auto fontForeground = fragmentFormat.foreground();
766 const auto fontBackground = fragmentFormat.background();
767
768 const QStringList families = fragmentFormat.fontFamilies().toStringList();
769 const auto fontFamily = families.isEmpty() ? QString() : families.constFirst();
770 const auto fontPointSize = fragmentFormat.font().pointSize();
771 const auto anchorHref = fragmentFormat.anchorHref();
772
773 const auto vAlign = fragmentFormat.verticalAlignment();
774 const auto superscript = (vAlign == QTextCharFormat::AlignSuperScript);
775 const auto subscript = (vAlign == QTextCharFormat::AlignSubScript);
776
777 if (superscript && !(d->m_openElements.contains(SuperScript))) {
778 d->m_elementsToOpen.insert(SuperScript);
779 }
780
781 if (subscript && !(d->m_openElements.contains(SubScript))) {
782 d->m_elementsToOpen.insert(SubScript);
783 }
784
785 if (!anchorHref.isEmpty() && !(d->m_openElements.contains(Anchor)) && (d->m_openAnchorHref != anchorHref)) {
786 d->m_elementsToOpen.insert(Anchor);
787 d->m_anchorHrefToOpen = anchorHref;
788 }
789
790 if (fontForeground != Qt::NoBrush
791 && !(d->m_openElements.contains(SpanForeground)) // Can only open one
792 // foreground element
793 // at a time.
794 && (fontForeground != d->m_openForeground)
795 && !((d->m_openElements.contains(Anchor) // Links can't have a foreground color.
796 || d->m_elementsToOpen.contains(Anchor)))) {
797 d->m_elementsToOpen.insert(SpanForeground);
798 d->m_foregroundToOpen = fontForeground;
799 }
800
801 if (fontBackground != Qt::NoBrush && !(d->m_openElements.contains(SpanBackground)) && (fontBackground != d->m_openBackground)) {
802 d->m_elementsToOpen.insert(SpanBackground);
803 d->m_backgroundToOpen = fontBackground;
804 }
805
806 if (!fontFamily.isEmpty() && !(d->m_openElements.contains(SpanFontFamily)) && (fontFamily != d->m_openFontFamily)) {
807 d->m_elementsToOpen.insert(SpanFontFamily);
808 d->m_fontFamilyToOpen = fontFamily;
809 }
810
811 if ((QTextCharFormat().font().pointSize() != fontPointSize) // Different from the default.
812 && !(d->m_openElements.contains(SpanFontPointSize)) && (fontPointSize != d->m_openFontPointSize)) {
813 d->m_elementsToOpen.insert(SpanFontPointSize);
814 d->m_fontPointSizeToOpen = fontPointSize;
815 }
816
817 // Only open a new bold tag if one is not already open.
818 // eg, <b>some <i>mixed</i> format</b> should be as is, rather than
819 // <b>some </b><b><i>mixed</i></b><b> format</b>
820
821 if (fontWeight >= QFont::DemiBold && !(d->m_openElements.contains(Strong))) {
822 d->m_elementsToOpen.insert(Strong);
823 }
824
825 if (fontItalic && !(d->m_openElements.contains(Emph))) {
826 d->m_elementsToOpen.insert(Emph);
827 }
828
829 if (fontUnderline && !(d->m_openElements.contains(Underline))
830 && !(d->m_openElements.contains(Anchor) || d->m_elementsToOpen.contains(Anchor)) // Can't change the underline state of a link.
831 ) {
832 d->m_elementsToOpen.insert(Underline);
833 }
834
835 if (fontStrikeout && !(d->m_openElements.contains(StrikeOut))) {
836 d->m_elementsToOpen.insert(StrikeOut);
837 }
838
839 if (d->m_elementsToOpen.size() <= 1) {
840 return d->m_elementsToOpen.values();
841 }
842 return sortOpeningOrder(d->m_elementsToOpen, it);
843}
844
846{
847 QList<int> sortedOpenedElements;
848
849 // This is an insertion sort in a way. elements in openingOrder are assumed
850 // to
851 // be out of order.
852 // The rest of the block is traversed until there are no more elements to
853 // sort, or the end is reached.
854 while (!openingOrder.isEmpty()) {
855 if (!it.atEnd()) {
856 it++;
857
858 if (!it.atEnd()) {
859 // Because I've iterated, this returns the elements that will
860 // be closed by the next fragment.
861 const auto elementsToClose = getElementsToClose(it);
862
863 // The exact order these are opened in is irrelevant, as all
864 // will be
865 // closed on the same block.
866 // See testDoubleFormat.
867 for (int tag : elementsToClose) {
868 if (openingOrder.remove(tag)) {
869 sortedOpenedElements.prepend(tag);
870 }
871 }
872 }
873 } else {
874 // End of block. Need to close all open elements.
875 // Order irrelevant in this case.
876 for (int tag : std::as_const(openingOrder)) {
877 sortedOpenedElements.prepend(tag);
878 }
879 break;
880 }
881 }
882 return sortedOpenedElements;
883}
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)
int pointSize() const const
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
QFont font() const const
QTextObject * objectForFormat(const QTextFormat &f) const const
QTextFrame * rootFrame() const const
BlockTrailingHorizontalRulerWidth
QTextImageFormat toImageFormat() const const
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 Tue Mar 26 2024 11:20:45 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.