1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtGui module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qtextdocumentfragment.h"
41#include "qtextdocumentfragment_p.h"
42#include "qtextcursor_p.h"
43#include "qtextlist.h"
44
45#include <qdebug.h>
46#include <qbytearray.h>
47#include <qdatastream.h>
48#include <qdatetime.h>
49
50QT_BEGIN_NAMESPACE
51
52QTextCopyHelper::QTextCopyHelper(const QTextCursor &_source, const QTextCursor &_destination, bool forceCharFormat, const QTextCharFormat &fmt)
53#if defined(Q_CC_DIAB) // compiler bug
54 : formatCollection(*_destination.d->priv->formatCollection()), originalText((const QString)_source.d->priv->buffer())
55#else
56 : formatCollection(*_destination.d->priv->formatCollection()), originalText(_source.d->priv->buffer())
57#endif
58{
59 src = _source.d->priv;
60 dst = _destination.d->priv;
61 insertPos = _destination.position();
62 this->forceCharFormat = forceCharFormat;
63 primaryCharFormatIndex = convertFormatIndex(fmt);
64 cursor = _source;
65}
66
67int QTextCopyHelper::convertFormatIndex(const QTextFormat &oldFormat, int objectIndexToSet)
68{
69 QTextFormat fmt = oldFormat;
70 if (objectIndexToSet != -1) {
71 fmt.setObjectIndex(objectIndexToSet);
72 } else if (fmt.objectIndex() != -1) {
73 int newObjectIndex = objectIndexMap.value(fmt.objectIndex(), -1);
74 if (newObjectIndex == -1) {
75 QTextFormat objFormat = src->formatCollection()->objectFormat(fmt.objectIndex());
76 Q_ASSERT(objFormat.objectIndex() == -1);
77 newObjectIndex = formatCollection.createObjectIndex(objFormat);
78 objectIndexMap.insert(fmt.objectIndex(), newObjectIndex);
79 }
80 fmt.setObjectIndex(newObjectIndex);
81 }
82 int idx = formatCollection.indexForFormat(fmt);
83 Q_ASSERT(formatCollection.format(idx).type() == oldFormat.type());
84 return idx;
85}
86
87int QTextCopyHelper::appendFragment(int pos, int endPos, int objectIndex)
88{
89 QTextDocumentPrivate::FragmentIterator fragIt = src->find(pos);
90 const QTextFragmentData * const frag = fragIt.value();
91
92 Q_ASSERT(objectIndex == -1
93 || (frag->size_array[0] == 1 && src->formatCollection()->format(frag->format).objectIndex() != -1));
94
95 int charFormatIndex;
96 if (forceCharFormat)
97 charFormatIndex = primaryCharFormatIndex;
98 else
99 charFormatIndex = convertFormatIndex(frag->format, objectIndex);
100
101 const int inFragmentOffset = qMax(0, pos - fragIt.position());
102 int charsToCopy = qMin(int(frag->size_array[0] - inFragmentOffset), endPos - pos);
103
104 QTextBlock nextBlock = src->blocksFind(pos + 1);
105
106 int blockIdx = -2;
107 if (nextBlock.position() == pos + 1) {
108 blockIdx = convertFormatIndex(nextBlock.blockFormat());
109 } else if (pos == 0 && insertPos == 0) {
110 dst->setBlockFormat(dst->blocksBegin(), dst->blocksBegin(), convertFormat(src->blocksBegin().blockFormat()).toBlockFormat());
111 dst->setCharFormat(-1, 1, convertFormat(src->blocksBegin().charFormat()).toCharFormat());
112 }
113
114 QString txtToInsert(originalText.constData() + frag->stringPosition + inFragmentOffset, charsToCopy);
115 if (txtToInsert.length() == 1
116 && (txtToInsert.at(0) == QChar::ParagraphSeparator
117 || txtToInsert.at(0) == QTextBeginningOfFrame
118 || txtToInsert.at(0) == QTextEndOfFrame
119 )
120 ) {
121 dst->insertBlock(txtToInsert.at(0), insertPos, blockIdx, charFormatIndex);
122 ++insertPos;
123 } else {
124 if (nextBlock.textList()) {
125 QTextBlock dstBlock = dst->blocksFind(insertPos);
126 if (!dstBlock.textList()) {
127 // insert a new text block with the block and char format from the
128 // source block to make sure that the following text fragments
129 // end up in a list as they should
130 int listBlockFormatIndex = convertFormatIndex(nextBlock.blockFormat());
131 int listCharFormatIndex = convertFormatIndex(nextBlock.charFormat());
132 dst->insertBlock(insertPos, listBlockFormatIndex, listCharFormatIndex);
133 ++insertPos;
134 }
135 }
136 dst->insert(insertPos, txtToInsert, charFormatIndex);
137 const int userState = nextBlock.userState();
138 if (userState != -1)
139 dst->blocksFind(insertPos).setUserState(userState);
140 insertPos += txtToInsert.length();
141 }
142
143 return charsToCopy;
144}
145
146void QTextCopyHelper::appendFragments(int pos, int endPos)
147{
148 Q_ASSERT(pos < endPos);
149
150 while (pos < endPos)
151 pos += appendFragment(pos, endPos);
152}
153
154void QTextCopyHelper::copy()
155{
156 if (cursor.hasComplexSelection()) {
157 QTextTable *table = cursor.currentTable();
158 int row_start, col_start, num_rows, num_cols;
159 cursor.selectedTableCells(&row_start, &num_rows, &col_start, &num_cols);
160
161 QTextTableFormat tableFormat = table->format();
162 tableFormat.setColumns(num_cols);
163 tableFormat.clearColumnWidthConstraints();
164 const int objectIndex = dst->formatCollection()->createObjectIndex(tableFormat);
165
166 Q_ASSERT(row_start != -1);
167 for (int r = row_start; r < row_start + num_rows; ++r) {
168 for (int c = col_start; c < col_start + num_cols; ++c) {
169 QTextTableCell cell = table->cellAt(r, c);
170 const int rspan = cell.rowSpan();
171 const int cspan = cell.columnSpan();
172 if (rspan != 1) {
173 int cr = cell.row();
174 if (cr != r)
175 continue;
176 }
177 if (cspan != 1) {
178 int cc = cell.column();
179 if (cc != c)
180 continue;
181 }
182
183 // add the QTextBeginningOfFrame
184 QTextCharFormat cellFormat = cell.format();
185 if (r + rspan >= row_start + num_rows) {
186 cellFormat.setTableCellRowSpan(row_start + num_rows - r);
187 }
188 if (c + cspan >= col_start + num_cols) {
189 cellFormat.setTableCellColumnSpan(col_start + num_cols - c);
190 }
191 const int charFormatIndex = convertFormatIndex(cellFormat, objectIndex);
192
193 int blockIdx = -2;
194 const int cellPos = cell.firstPosition();
195 QTextBlock block = src->blocksFind(cellPos);
196 if (block.position() == cellPos) {
197 blockIdx = convertFormatIndex(block.blockFormat());
198 }
199
200 dst->insertBlock(QTextBeginningOfFrame, insertPos, blockIdx, charFormatIndex);
201 ++insertPos;
202
203 // nothing to add for empty cells
204 if (cell.lastPosition() > cellPos) {
205 // add the contents
206 appendFragments(cellPos, cell.lastPosition());
207 }
208 }
209 }
210
211 // add end of table
212 int end = table->lastPosition();
213 appendFragment(end, end+1, objectIndex);
214 } else {
215 appendFragments(cursor.selectionStart(), cursor.selectionEnd());
216 }
217}
218
219QTextDocumentFragmentPrivate::QTextDocumentFragmentPrivate(const QTextCursor &_cursor)
220 : ref(1), doc(new QTextDocument), importedFromPlainText(false)
221{
222 doc->setUndoRedoEnabled(false);
223
224 if (!_cursor.hasSelection())
225 return;
226
227 QTextDocumentPrivate *p = QTextDocumentPrivate::get(doc);
228 p->beginEditBlock();
229 QTextCursor destCursor(doc);
230 QTextCopyHelper(_cursor, destCursor).copy();
231 p->endEditBlock();
232
233 if (_cursor.d)
234 p->mergeCachedResources(_cursor.d->priv);
235}
236
237void QTextDocumentFragmentPrivate::insert(QTextCursor &_cursor) const
238{
239 if (_cursor.isNull())
240 return;
241
242 QTextDocumentPrivate *destPieceTable = _cursor.d->priv;
243 destPieceTable->beginEditBlock();
244
245 QTextCursor sourceCursor(doc);
246 sourceCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
247 QTextCopyHelper(sourceCursor, _cursor, importedFromPlainText, _cursor.charFormat()).copy();
248
249 destPieceTable->endEditBlock();
250}
251
252/*!
253 \class QTextDocumentFragment
254 \reentrant
255
256 \inmodule QtGui
257 \brief The QTextDocumentFragment class represents a piece of formatted text
258 from a QTextDocument.
259
260 \ingroup richtext-processing
261 \ingroup shared
262
263 A QTextDocumentFragment is a fragment of rich text, that can be inserted into
264 a QTextDocument. A document fragment can be created from a
265 QTextDocument, from a QTextCursor's selection, or from another
266 document fragment. Document fragments can also be created by the
267 static functions, fromPlainText() and fromHtml().
268
269 The contents of a document fragment can be obtained as plain text
270 by using the toPlainText() function, or it can be obtained as HTML
271 with toHtml().
272*/
273
274
275/*!
276 Constructs an empty QTextDocumentFragment.
277
278 \sa isEmpty()
279*/
280QTextDocumentFragment::QTextDocumentFragment()
281 : d(nullptr)
282{
283}
284
285/*!
286 Converts the given \a document into a QTextDocumentFragment.
287 Note that the QTextDocumentFragment only stores the document contents, not meta information
288 like the document's title.
289*/
290QTextDocumentFragment::QTextDocumentFragment(const QTextDocument *document)
291 : d(nullptr)
292{
293 if (!document)
294 return;
295
296 QTextCursor cursor(const_cast<QTextDocument *>(document));
297 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
298 d = new QTextDocumentFragmentPrivate(cursor);
299}
300
301/*!
302 Creates a QTextDocumentFragment from the \a{cursor}'s selection.
303 If the cursor doesn't have a selection, the created fragment is empty.
304
305 \sa isEmpty(), QTextCursor::selection()
306*/
307QTextDocumentFragment::QTextDocumentFragment(const QTextCursor &cursor)
308 : d(nullptr)
309{
310 if (!cursor.hasSelection())
311 return;
312
313 d = new QTextDocumentFragmentPrivate(cursor);
314}
315
316/*!
317 \fn QTextDocumentFragment::QTextDocumentFragment(const QTextDocumentFragment &other)
318
319 Copy constructor. Creates a copy of the \a other fragment.
320*/
321QTextDocumentFragment::QTextDocumentFragment(const QTextDocumentFragment &rhs)
322 : d(rhs.d)
323{
324 if (d)
325 d->ref.ref();
326}
327
328/*!
329 \fn QTextDocumentFragment &QTextDocumentFragment::operator=(const QTextDocumentFragment &other)
330
331 Assigns the \a other fragment to this fragment.
332*/
333QTextDocumentFragment &QTextDocumentFragment::operator=(const QTextDocumentFragment &rhs)
334{
335 if (rhs.d)
336 rhs.d->ref.ref();
337 if (d && !d->ref.deref())
338 delete d;
339 d = rhs.d;
340 return *this;
341}
342
343/*!
344 Destroys the document fragment.
345*/
346QTextDocumentFragment::~QTextDocumentFragment()
347{
348 if (d && !d->ref.deref())
349 delete d;
350}
351
352/*!
353 Returns \c true if the fragment is empty; otherwise returns \c false.
354*/
355bool QTextDocumentFragment::isEmpty() const
356{
357 return d == nullptr || d->doc == nullptr || QTextDocumentPrivate::get(d->doc)->length() <= 1;
358}
359
360/*!
361 Returns the document fragment's text as plain text (i.e. with no
362 formatting information).
363
364 \sa toHtml()
365*/
366QString QTextDocumentFragment::toPlainText() const
367{
368 if (!d)
369 return QString();
370
371 return d->doc->toPlainText();
372}
373
374#ifndef QT_NO_TEXTHTMLPARSER
375
376/*!
377 \since 4.2
378
379 Returns the contents of the document fragment as HTML.
380
381 \sa toPlainText(), QTextDocument::toHtml()
382*/
383QString QTextDocumentFragment::toHtml() const
384{
385 if (!d)
386 return QString();
387
388 return QTextHtmlExporter(d->doc).toHtml(QTextHtmlExporter::ExportFragment);
389}
390
391#endif // QT_NO_TEXTHTMLPARSER
392
393/*!
394 Returns a document fragment that contains the given \a plainText.
395
396 When inserting such a fragment into a QTextDocument the current char format of
397 the QTextCursor used for insertion is used as format for the text.
398*/
399QTextDocumentFragment QTextDocumentFragment::fromPlainText(const QString &plainText)
400{
401 QTextDocumentFragment res;
402
403 res.d = new QTextDocumentFragmentPrivate;
404 res.d->importedFromPlainText = true;
405 QTextCursor cursor(res.d->doc);
406 cursor.insertText(plainText);
407 return res;
408}
409
410#ifndef QT_NO_TEXTHTMLPARSER
411
412static QTextListFormat::Style nextListStyle(QTextListFormat::Style style)
413{
414 if (style == QTextListFormat::ListDisc)
415 return QTextListFormat::ListCircle;
416 else if (style == QTextListFormat::ListCircle)
417 return QTextListFormat::ListSquare;
418 return style;
419}
420
421QTextHtmlImporter::QTextHtmlImporter(QTextDocument *_doc, const QString &_html, ImportMode mode, const QTextDocument *resourceProvider)
422 : indent(0), headingLevel(0), compressNextWhitespace(PreserveWhiteSpace), doc(_doc), importMode(mode)
423{
424 cursor = QTextCursor(doc);
425 wsm = QTextHtmlParserNode::WhiteSpaceNormal;
426
427 QString html = _html;
428 const int startFragmentPos = html.indexOf(QLatin1String("<!--StartFragment-->"));
429 if (startFragmentPos != -1) {
430 const QLatin1String qt3RichTextHeader("<meta name=\"qrichtext\" content=\"1\" />");
431
432 // Hack for Qt3
433 const bool hasQtRichtextMetaTag = html.contains(qt3RichTextHeader);
434
435 const int endFragmentPos = html.indexOf(QLatin1String("<!--EndFragment-->"));
436 if (startFragmentPos < endFragmentPos)
437 html = html.mid(startFragmentPos, endFragmentPos - startFragmentPos);
438 else
439 html = html.mid(startFragmentPos);
440
441 if (hasQtRichtextMetaTag)
442 html.prepend(qt3RichTextHeader);
443 }
444
445 parse(html, resourceProvider ? resourceProvider : doc);
446// dumpHtml();
447}
448
449void QTextHtmlImporter::import()
450{
451 cursor.beginEditBlock();
452 hasBlock = true;
453 forceBlockMerging = false;
454 compressNextWhitespace = RemoveWhiteSpace;
455 blockTagClosed = false;
456 for (currentNodeIdx = 0; currentNodeIdx < count(); ++currentNodeIdx) {
457 currentNode = &at(currentNodeIdx);
458 wsm = textEditMode ? QTextHtmlParserNode::WhiteSpacePreWrap : currentNode->wsm;
459
460 /*
461 * process each node in three stages:
462 * 1) check if the hierarchy changed and we therefore passed the
463 * equivalent of a closing tag -> we may need to finish off
464 * some structures like tables
465 *
466 * 2) check if the current node is a special node like a
467 * <table>, <ul> or <img> tag that requires special processing
468 *
469 * 3) if the node should result in a QTextBlock create one and
470 * finally insert text that may be attached to the node
471 */
472
473 /* emit 'closing' table blocks or adjust current indent level
474 * if we
475 * 1) are beyond the first node
476 * 2) the current node not being a child of the previous node
477 * means there was a tag closing in the input html
478 */
479 if (currentNodeIdx > 0 && (currentNode->parent != currentNodeIdx - 1)) {
480 blockTagClosed = closeTag();
481 // visually collapse subsequent block tags, but if the element after the closed block tag
482 // is for example an inline element (!isBlock) we have to make sure we start a new paragraph by setting
483 // hasBlock to false.
484 if (blockTagClosed
485 && !currentNode->isBlock()
486 && currentNode->id != Html_unknown)
487 {
488 hasBlock = false;
489 } else if (blockTagClosed && hasBlock) {
490 // when collapsing subsequent block tags we need to clear the block format
491 QTextBlockFormat blockFormat = currentNode->blockFormat;
492 blockFormat.setIndent(indent);
493
494 QTextBlockFormat oldFormat = cursor.blockFormat();
495 if (oldFormat.hasProperty(QTextFormat::PageBreakPolicy)) {
496 QTextFormat::PageBreakFlags pageBreak = oldFormat.pageBreakPolicy();
497 if (pageBreak == QTextFormat::PageBreak_AlwaysAfter)
498 /* We remove an empty paragrah that requested a page break after.
499 moving that request to the next paragraph means we also need to make
500 that a pagebreak before to keep the same visual appearance.
501 */
502 pageBreak = QTextFormat::PageBreak_AlwaysBefore;
503 blockFormat.setPageBreakPolicy(pageBreak);
504 }
505
506 cursor.setBlockFormat(blockFormat);
507 }
508 }
509
510 if (currentNode->displayMode == QTextHtmlElement::DisplayNone) {
511 if (currentNode->id == Html_title)
512 doc->setMetaInformation(QTextDocument::DocumentTitle, currentNode->text);
513 // ignore explicitly 'invisible' elements
514 continue;
515 }
516
517 if (processSpecialNodes() == ContinueWithNextNode)
518 continue;
519
520 // make sure there's a block for 'Blah' after <ul><li>foo</ul>Blah
521 if (blockTagClosed
522 && !hasBlock
523 && !currentNode->isBlock()
524 && !currentNode->text.isEmpty() && !currentNode->hasOnlyWhitespace()
525 && currentNode->displayMode == QTextHtmlElement::DisplayInline) {
526
527 QTextBlockFormat block = currentNode->blockFormat;
528 block.setIndent(indent);
529
530 appendBlock(block, currentNode->charFormat);
531
532 hasBlock = true;
533 }
534
535 if (currentNode->isBlock()) {
536 QTextHtmlImporter::ProcessNodeResult result = processBlockNode();
537 if (result == ContinueWithNextNode) {
538 continue;
539 } else if (result == ContinueWithNextSibling) {
540 currentNodeIdx += currentNode->children.size();
541 continue;
542 }
543 }
544
545 if (currentNode->charFormat.isAnchor()) {
546 const auto names = currentNode->charFormat.anchorNames();
547 if (!names.isEmpty())
548 namedAnchors.append(names.constFirst());
549 }
550
551 if (appendNodeText())
552 hasBlock = false; // if we actually appended text then we don't
553 // have an empty block anymore
554 }
555
556 cursor.endEditBlock();
557}
558
559bool QTextHtmlImporter::appendNodeText()
560{
561 const int initialCursorPosition = cursor.position();
562 QTextCharFormat format = currentNode->charFormat;
563
564 if(wsm == QTextHtmlParserNode::WhiteSpacePre || wsm == QTextHtmlParserNode::WhiteSpacePreWrap)
565 compressNextWhitespace = PreserveWhiteSpace;
566
567 QString text = currentNode->text;
568
569 QString textToInsert;
570 textToInsert.reserve(text.size());
571
572 for (int i = 0; i < text.length(); ++i) {
573 QChar ch = text.at(i);
574
575 if (ch.isSpace()
576 && ch != QChar::Nbsp
577 && ch != QChar::ParagraphSeparator) {
578
579 if (wsm == QTextHtmlParserNode::WhiteSpacePreLine && (ch == QLatin1Char('\n') || ch == QLatin1Char('\r')))
580 compressNextWhitespace = PreserveWhiteSpace;
581
582 if (compressNextWhitespace == CollapseWhiteSpace)
583 compressNextWhitespace = RemoveWhiteSpace; // allow this one, and remove the ones coming next.
584 else if(compressNextWhitespace == RemoveWhiteSpace)
585 continue;
586
587 if (wsm == QTextHtmlParserNode::WhiteSpacePre
588 || textEditMode
589 ) {
590 if (ch == QLatin1Char('\n')) {
591 if (textEditMode)
592 continue;
593 } else if (ch == QLatin1Char('\r')) {
594 continue;
595 }
596 } else if (wsm != QTextHtmlParserNode::WhiteSpacePreWrap) {
597 compressNextWhitespace = RemoveWhiteSpace;
598 if (wsm == QTextHtmlParserNode::WhiteSpacePreLine && (ch == QLatin1Char('\n') || ch == QLatin1Char('\r')))
599 { }
600 else if (wsm == QTextHtmlParserNode::WhiteSpaceNoWrap)
601 ch = QChar::Nbsp;
602 else
603 ch = QLatin1Char(' ');
604 }
605 } else {
606 compressNextWhitespace = PreserveWhiteSpace;
607 }
608
609 if (ch == QLatin1Char('\n')
610 || ch == QChar::ParagraphSeparator) {
611
612 if (!textToInsert.isEmpty()) {
613 if (wsm == QTextHtmlParserNode::WhiteSpacePreLine && textToInsert.at(textToInsert.length() - 1) == QLatin1Char(' '))
614 textToInsert = textToInsert.chopped(1);
615 cursor.insertText(textToInsert, format);
616 textToInsert.clear();
617 }
618
619 QTextBlockFormat fmt = cursor.blockFormat();
620
621 if (fmt.hasProperty(QTextFormat::BlockBottomMargin)) {
622 QTextBlockFormat tmp = fmt;
623 tmp.clearProperty(QTextFormat::BlockBottomMargin);
624 cursor.setBlockFormat(tmp);
625 }
626
627 fmt.clearProperty(QTextFormat::BlockTopMargin);
628 appendBlock(fmt, cursor.charFormat());
629 } else {
630 if (!namedAnchors.isEmpty()) {
631 if (!textToInsert.isEmpty()) {
632 cursor.insertText(textToInsert, format);
633 textToInsert.clear();
634 }
635
636 format.setAnchor(true);
637 format.setAnchorNames(namedAnchors);
638 cursor.insertText(ch, format);
639 namedAnchors.clear();
640 format.clearProperty(QTextFormat::IsAnchor);
641 format.clearProperty(QTextFormat::AnchorName);
642 } else {
643 textToInsert += ch;
644 }
645 }
646 }
647
648 if (!textToInsert.isEmpty()) {
649 cursor.insertText(textToInsert, format);
650 }
651
652 return cursor.position() != initialCursorPosition;
653}
654
655QTextHtmlImporter::ProcessNodeResult QTextHtmlImporter::processSpecialNodes()
656{
657 switch (currentNode->id) {
658 case Html_body:
659 if (currentNode->charFormat.background().style() != Qt::NoBrush) {
660 QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
661 fmt.setBackground(currentNode->charFormat.background());
662 doc->rootFrame()->setFrameFormat(fmt);
663 const_cast<QTextHtmlParserNode *>(currentNode)->charFormat.clearProperty(QTextFormat::BackgroundBrush);
664 }
665 compressNextWhitespace = RemoveWhiteSpace;
666 break;
667
668 case Html_ol:
669 case Html_ul: {
670 QTextListFormat::Style style = currentNode->listStyle;
671
672 if (currentNode->id == Html_ul && !currentNode->hasOwnListStyle && currentNode->parent) {
673 const QTextHtmlParserNode *n = &at(currentNode->parent);
674 while (n) {
675 if (n->id == Html_ul) {
676 style = nextListStyle(currentNode->listStyle);
677 }
678 if (n->parent)
679 n = &at(n->parent);
680 else
681 n = nullptr;
682 }
683 }
684
685 QTextListFormat listFmt;
686 listFmt.setStyle(style);
687 if (!currentNode->textListNumberPrefix.isNull())
688 listFmt.setNumberPrefix(currentNode->textListNumberPrefix);
689 if (!currentNode->textListNumberSuffix.isNull())
690 listFmt.setNumberSuffix(currentNode->textListNumberSuffix);
691
692 ++indent;
693 if (currentNode->hasCssListIndent)
694 listFmt.setIndent(currentNode->cssListIndent);
695 else
696 listFmt.setIndent(indent);
697
698 List l;
699 l.format = listFmt;
700 l.listNode = currentNodeIdx;
701 lists.append(l);
702 compressNextWhitespace = RemoveWhiteSpace;
703
704 // broken html: <ul>Text here<li>Foo
705 const QString simpl = currentNode->text.simplified();
706 if (simpl.isEmpty() || simpl.at(0).isSpace())
707 return ContinueWithNextNode;
708 break;
709 }
710
711 case Html_table: {
712 Table t = scanTable(currentNodeIdx);
713 tables.append(t);
714 hasBlock = false;
715 compressNextWhitespace = RemoveWhiteSpace;
716 return ContinueWithNextNode;
717 }
718
719 case Html_tr:
720 return ContinueWithNextNode;
721
722 case Html_img: {
723 QTextImageFormat fmt;
724 fmt.setName(currentNode->imageName);
725 if (!currentNode->text.isEmpty())
726 fmt.setProperty(QTextFormat::ImageTitle, currentNode->text);
727 if (!currentNode->imageAlt.isEmpty())
728 fmt.setProperty(QTextFormat::ImageAltText, currentNode->imageAlt);
729
730 fmt.merge(currentNode->charFormat);
731
732 if (currentNode->imageWidth != -1)
733 fmt.setWidth(currentNode->imageWidth);
734 if (currentNode->imageHeight != -1)
735 fmt.setHeight(currentNode->imageHeight);
736
737 cursor.insertImage(fmt, QTextFrameFormat::Position(currentNode->cssFloat));
738
739 cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
740 cursor.mergeCharFormat(currentNode->charFormat);
741 cursor.movePosition(QTextCursor::NextCharacter);
742 compressNextWhitespace = CollapseWhiteSpace;
743
744 hasBlock = false;
745 return ContinueWithNextNode;
746 }
747
748 case Html_hr: {
749 QTextBlockFormat blockFormat = currentNode->blockFormat;
750 blockFormat.setTopMargin(topMargin(currentNodeIdx));
751 blockFormat.setBottomMargin(bottomMargin(currentNodeIdx));
752 blockFormat.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, currentNode->width);
753 if (hasBlock && importMode == ImportToDocument)
754 cursor.mergeBlockFormat(blockFormat);
755 else
756 appendBlock(blockFormat);
757 hasBlock = false;
758 compressNextWhitespace = RemoveWhiteSpace;
759 return ContinueWithNextNode;
760 }
761
762 case Html_h1:
763 headingLevel = 1;
764 break;
765 case Html_h2:
766 headingLevel = 2;
767 break;
768 case Html_h3:
769 headingLevel = 3;
770 break;
771 case Html_h4:
772 headingLevel = 4;
773 break;
774 case Html_h5:
775 headingLevel = 5;
776 break;
777 case Html_h6:
778 headingLevel = 6;
779 break;
780
781 default: break;
782 }
783
784 return ContinueWithCurrentNode;
785}
786
787// returns true if a block tag was closed
788bool QTextHtmlImporter::closeTag()
789{
790 const QTextHtmlParserNode *closedNode = &at(currentNodeIdx - 1);
791 const int endDepth = depth(currentNodeIdx) - 1;
792 int depth = this->depth(currentNodeIdx - 1);
793 bool blockTagClosed = false;
794
795 while (depth > endDepth) {
796 Table *t = nullptr;
797 if (!tables.isEmpty())
798 t = &tables.last();
799
800 switch (closedNode->id) {
801 case Html_tr:
802 if (t && !t->isTextFrame) {
803 ++t->currentRow;
804
805 // for broken html with rowspans but missing tr tags
806 while (!t->currentCell.atEnd() && t->currentCell.row < t->currentRow)
807 ++t->currentCell;
808 }
809
810 blockTagClosed = true;
811 break;
812
813 case Html_table:
814 if (!t)
815 break;
816 indent = t->lastIndent;
817
818 tables.resize(tables.size() - 1);
819 t = nullptr;
820
821 if (tables.isEmpty()) {
822 cursor = doc->rootFrame()->lastCursorPosition();
823 } else {
824 t = &tables.last();
825 if (t->isTextFrame)
826 cursor = t->frame->lastCursorPosition();
827 else if (!t->currentCell.atEnd())
828 cursor = t->currentCell.cell().lastCursorPosition();
829 }
830
831 // we don't need an extra block after tables, so we don't
832 // claim to have closed one for the creation of a new one
833 // in import()
834 blockTagClosed = false;
835 compressNextWhitespace = RemoveWhiteSpace;
836 break;
837
838 case Html_th:
839 case Html_td:
840 if (t && !t->isTextFrame)
841 ++t->currentCell;
842 blockTagClosed = true;
843 compressNextWhitespace = RemoveWhiteSpace;
844 break;
845
846 case Html_ol:
847 case Html_ul:
848 if (lists.isEmpty())
849 break;
850 lists.resize(lists.size() - 1);
851 --indent;
852 blockTagClosed = true;
853 break;
854
855 case Html_br:
856 compressNextWhitespace = RemoveWhiteSpace;
857 break;
858
859 case Html_div:
860 if (cursor.position() > 0) {
861 const QChar curChar = cursor.document()->characterAt(cursor.position() - 1);
862 if (!closedNode->children.isEmpty() && curChar != QChar::LineSeparator) {
863 blockTagClosed = true;
864 }
865 }
866 break;
867 case Html_h1:
868 case Html_h2:
869 case Html_h3:
870 case Html_h4:
871 case Html_h5:
872 case Html_h6:
873 headingLevel = 0;
874 blockTagClosed = true;
875 break;
876 default:
877 if (closedNode->isBlock())
878 blockTagClosed = true;
879 break;
880 }
881
882 closedNode = &at(closedNode->parent);
883 --depth;
884 }
885
886 return blockTagClosed;
887}
888
889QTextHtmlImporter::Table QTextHtmlImporter::scanTable(int tableNodeIdx)
890{
891 Table table;
892 table.columns = 0;
893
894 QList<QTextLength> columnWidths;
895
896 int tableHeaderRowCount = 0;
897 QList<int> rowNodes;
898 rowNodes.reserve(at(tableNodeIdx).children.count());
899 for (int row : at(tableNodeIdx).children) {
900 switch (at(row).id) {
901 case Html_tr:
902 rowNodes += row;
903 break;
904 case Html_thead:
905 case Html_tbody:
906 case Html_tfoot:
907 for (int potentialRow : at(row).children) {
908 if (at(potentialRow).id == Html_tr) {
909 rowNodes += potentialRow;
910 if (at(row).id == Html_thead)
911 ++tableHeaderRowCount;
912 }
913 }
914 break;
915 default: break;
916 }
917 }
918
919 QList<RowColSpanInfo> rowColSpans;
920 QList<RowColSpanInfo> rowColSpanForColumn;
921
922 int effectiveRow = 0;
923 for (int row : qAsConst(rowNodes)) {
924 int colsInRow = 0;
925
926 for (int cell : at(row).children) {
927 if (at(cell).isTableCell()) {
928 // skip all columns with spans from previous rows
929 while (colsInRow < rowColSpanForColumn.size()) {
930 const RowColSpanInfo &spanInfo = rowColSpanForColumn.at(colsInRow);
931
932 if (spanInfo.row + spanInfo.rowSpan > effectiveRow) {
933 Q_ASSERT(spanInfo.col == colsInRow);
934 colsInRow += spanInfo.colSpan;
935 } else
936 break;
937 }
938
939 const QTextHtmlParserNode &c = at(cell);
940 const int currentColumn = colsInRow;
941 colsInRow += c.tableCellColSpan;
942
943 RowColSpanInfo spanInfo;
944 spanInfo.row = effectiveRow;
945 spanInfo.col = currentColumn;
946 spanInfo.colSpan = c.tableCellColSpan;
947 spanInfo.rowSpan = c.tableCellRowSpan;
948 if (spanInfo.colSpan > 1 || spanInfo.rowSpan > 1)
949 rowColSpans.append(spanInfo);
950
951 columnWidths.resize(qMax(columnWidths.count(), colsInRow));
952 rowColSpanForColumn.resize(columnWidths.size());
953 for (int i = currentColumn; i < currentColumn + c.tableCellColSpan; ++i) {
954 if (columnWidths.at(i).type() == QTextLength::VariableLength) {
955 QTextLength w = c.width;
956 if (c.tableCellColSpan > 1 && w.type() != QTextLength::VariableLength)
957 w = QTextLength(w.type(), w.value(100.) / c.tableCellColSpan);
958 columnWidths[i] = w;
959 }
960 rowColSpanForColumn[i] = spanInfo;
961 }
962 }
963 }
964
965 table.columns = qMax(table.columns, colsInRow);
966
967 ++effectiveRow;
968 }
969 table.rows = effectiveRow;
970
971 table.lastIndent = indent;
972 indent = 0;
973
974 if (table.rows == 0 || table.columns == 0)
975 return table;
976
977 QTextFrameFormat fmt;
978 const QTextHtmlParserNode &node = at(tableNodeIdx);
979
980 if (!node.isTextFrame) {
981 QTextTableFormat tableFmt;
982 tableFmt.setCellSpacing(node.tableCellSpacing);
983 tableFmt.setCellPadding(node.tableCellPadding);
984 if (node.blockFormat.hasProperty(QTextFormat::BlockAlignment))
985 tableFmt.setAlignment(node.blockFormat.alignment());
986 tableFmt.setColumns(table.columns);
987 tableFmt.setColumnWidthConstraints(columnWidths);
988 tableFmt.setHeaderRowCount(tableHeaderRowCount);
989 tableFmt.setBorderCollapse(node.borderCollapse);
990 fmt = tableFmt;
991 }
992
993 fmt.setTopMargin(topMargin(tableNodeIdx));
994 fmt.setBottomMargin(bottomMargin(tableNodeIdx));
995 fmt.setLeftMargin(leftMargin(tableNodeIdx)
996 + table.lastIndent * 40 // ##### not a good emulation
997 );
998 fmt.setRightMargin(rightMargin(tableNodeIdx));
999
1000 // compatibility
1001 if (qFuzzyCompare(fmt.leftMargin(), fmt.rightMargin())
1002 && qFuzzyCompare(fmt.leftMargin(), fmt.topMargin())
1003 && qFuzzyCompare(fmt.leftMargin(), fmt.bottomMargin()))
1004 fmt.setProperty(QTextFormat::FrameMargin, fmt.leftMargin());
1005
1006 fmt.setBorderStyle(node.borderStyle);
1007 fmt.setBorderBrush(node.borderBrush);
1008 fmt.setBorder(node.tableBorder);
1009 fmt.setWidth(node.width);
1010 fmt.setHeight(node.height);
1011 if (node.blockFormat.hasProperty(QTextFormat::PageBreakPolicy))
1012 fmt.setPageBreakPolicy(node.blockFormat.pageBreakPolicy());
1013
1014 if (node.blockFormat.hasProperty(QTextFormat::LayoutDirection))
1015 fmt.setLayoutDirection(node.blockFormat.layoutDirection());
1016 if (node.charFormat.background().style() != Qt::NoBrush)
1017 fmt.setBackground(node.charFormat.background());
1018 fmt.setPosition(QTextFrameFormat::Position(node.cssFloat));
1019
1020 if (node.isTextFrame) {
1021 if (node.isRootFrame) {
1022 table.frame = cursor.currentFrame();
1023 table.frame->setFrameFormat(fmt);
1024 } else
1025 table.frame = cursor.insertFrame(fmt);
1026
1027 table.isTextFrame = true;
1028 } else {
1029 const int oldPos = cursor.position();
1030 QTextTable *textTable = cursor.insertTable(table.rows, table.columns, fmt.toTableFormat());
1031 table.frame = textTable;
1032
1033 for (int i = 0; i < rowColSpans.count(); ++i) {
1034 const RowColSpanInfo &nfo = rowColSpans.at(i);
1035 textTable->mergeCells(nfo.row, nfo.col, nfo.rowSpan, nfo.colSpan);
1036 }
1037
1038 table.currentCell = TableCellIterator(textTable);
1039 cursor.setPosition(oldPos); // restore for caption support which needs to be inserted right before the table
1040 }
1041 return table;
1042}
1043
1044QTextHtmlImporter::ProcessNodeResult QTextHtmlImporter::processBlockNode()
1045{
1046 QTextBlockFormat block;
1047 QTextCharFormat charFmt;
1048 bool modifiedBlockFormat = true;
1049 bool modifiedCharFormat = true;
1050
1051 if (currentNode->isTableCell() && !tables.isEmpty()) {
1052 Table &t = tables.last();
1053 if (!t.isTextFrame && !t.currentCell.atEnd()) {
1054 QTextTableCell cell = t.currentCell.cell();
1055 if (cell.isValid()) {
1056 QTextTableCellFormat fmt = cell.format().toTableCellFormat();
1057 if (topPadding(currentNodeIdx) >= 0)
1058 fmt.setTopPadding(topPadding(currentNodeIdx));
1059 if (bottomPadding(currentNodeIdx) >= 0)
1060 fmt.setBottomPadding(bottomPadding(currentNodeIdx));
1061 if (leftPadding(currentNodeIdx) >= 0)
1062 fmt.setLeftPadding(leftPadding(currentNodeIdx));
1063 if (rightPadding(currentNodeIdx) >= 0)
1064 fmt.setRightPadding(rightPadding(currentNodeIdx));
1065#ifndef QT_NO_CSSPARSER
1066 if (tableCellBorder(currentNodeIdx, QCss::TopEdge) > 0)
1067 fmt.setTopBorder(tableCellBorder(currentNodeIdx, QCss::TopEdge));
1068 if (tableCellBorder(currentNodeIdx, QCss::RightEdge) > 0)
1069 fmt.setRightBorder(tableCellBorder(currentNodeIdx, QCss::RightEdge));
1070 if (tableCellBorder(currentNodeIdx, QCss::BottomEdge) > 0)
1071 fmt.setBottomBorder(tableCellBorder(currentNodeIdx, QCss::BottomEdge));
1072 if (tableCellBorder(currentNodeIdx, QCss::LeftEdge) > 0)
1073 fmt.setLeftBorder(tableCellBorder(currentNodeIdx, QCss::LeftEdge));
1074 if (tableCellBorderStyle(currentNodeIdx, QCss::TopEdge) != QTextFrameFormat::BorderStyle_None)
1075 fmt.setTopBorderStyle(tableCellBorderStyle(currentNodeIdx, QCss::TopEdge));
1076 if (tableCellBorderStyle(currentNodeIdx, QCss::RightEdge) != QTextFrameFormat::BorderStyle_None)
1077 fmt.setRightBorderStyle(tableCellBorderStyle(currentNodeIdx, QCss::RightEdge));
1078 if (tableCellBorderStyle(currentNodeIdx, QCss::BottomEdge) != QTextFrameFormat::BorderStyle_None)
1079 fmt.setBottomBorderStyle(tableCellBorderStyle(currentNodeIdx, QCss::BottomEdge));
1080 if (tableCellBorderStyle(currentNodeIdx, QCss::LeftEdge) != QTextFrameFormat::BorderStyle_None)
1081 fmt.setLeftBorderStyle(tableCellBorderStyle(currentNodeIdx, QCss::LeftEdge));
1082 if (tableCellBorderBrush(currentNodeIdx, QCss::TopEdge) != Qt::NoBrush)
1083 fmt.setTopBorderBrush(tableCellBorderBrush(currentNodeIdx, QCss::TopEdge));
1084 if (tableCellBorderBrush(currentNodeIdx, QCss::RightEdge) != Qt::NoBrush)
1085 fmt.setRightBorderBrush(tableCellBorderBrush(currentNodeIdx, QCss::RightEdge));
1086 if (tableCellBorderBrush(currentNodeIdx, QCss::BottomEdge) != Qt::NoBrush)
1087 fmt.setBottomBorderBrush(tableCellBorderBrush(currentNodeIdx, QCss::BottomEdge));
1088 if (tableCellBorderBrush(currentNodeIdx, QCss::LeftEdge) != Qt::NoBrush)
1089 fmt.setLeftBorderBrush(tableCellBorderBrush(currentNodeIdx, QCss::LeftEdge));
1090#endif
1091
1092 cell.setFormat(fmt);
1093
1094 cursor.setPosition(cell.firstPosition());
1095 }
1096 }
1097 hasBlock = true;
1098 compressNextWhitespace = RemoveWhiteSpace;
1099
1100 if (currentNode->charFormat.background().style() != Qt::NoBrush) {
1101 charFmt.setBackground(currentNode->charFormat.background());
1102 cursor.mergeBlockCharFormat(charFmt);
1103 }
1104 }
1105
1106 if (hasBlock) {
1107 block = cursor.blockFormat();
1108 charFmt = cursor.blockCharFormat();
1109 modifiedBlockFormat = false;
1110 modifiedCharFormat = false;
1111 }
1112
1113 // collapse
1114 {
1115 qreal tm = qreal(topMargin(currentNodeIdx));
1116 if (tm > block.topMargin()) {
1117 block.setTopMargin(tm);
1118 modifiedBlockFormat = true;
1119 }
1120 }
1121
1122 int bottomMargin = this->bottomMargin(currentNodeIdx);
1123
1124 // for list items we may want to collapse with the bottom margin of the
1125 // list.
1126 const QTextHtmlParserNode *parentNode = currentNode->parent ? &at(currentNode->parent) : nullptr;
1127 if ((currentNode->id == Html_li || currentNode->id == Html_dt || currentNode->id == Html_dd)
1128 && parentNode
1129 && (parentNode->isListStart() || parentNode->id == Html_dl)
1130 && (parentNode->children.last() == currentNodeIdx)) {
1131 bottomMargin = qMax(bottomMargin, this->bottomMargin(currentNode->parent));
1132 }
1133
1134 if (block.bottomMargin() != bottomMargin) {
1135 block.setBottomMargin(bottomMargin);
1136 modifiedBlockFormat = true;
1137 }
1138
1139 {
1140 const qreal lm = leftMargin(currentNodeIdx);
1141 const qreal rm = rightMargin(currentNodeIdx);
1142
1143 if (block.leftMargin() != lm) {
1144 block.setLeftMargin(lm);
1145 modifiedBlockFormat = true;
1146 }
1147 if (block.rightMargin() != rm) {
1148 block.setRightMargin(rm);
1149 modifiedBlockFormat = true;
1150 }
1151 }
1152
1153 if (currentNode->id != Html_li
1154 && indent != 0
1155 && (lists.isEmpty()
1156 || !hasBlock
1157 || !lists.constLast().list
1158 || lists.constLast().list->itemNumber(cursor.block()) == -1
1159 )
1160 ) {
1161 block.setIndent(indent);
1162 modifiedBlockFormat = true;
1163 }
1164
1165 if (headingLevel) {
1166 block.setHeadingLevel(headingLevel);
1167 modifiedBlockFormat = true;
1168 }
1169
1170 if (currentNode->blockFormat.propertyCount() > 0) {
1171 modifiedBlockFormat = true;
1172 block.merge(currentNode->blockFormat);
1173 }
1174
1175 if (currentNode->charFormat.propertyCount() > 0) {
1176 modifiedCharFormat = true;
1177 charFmt.merge(currentNode->charFormat);
1178 }
1179
1180 // ####################
1181 // block.setFloatPosition(node->cssFloat);
1182
1183 if (wsm == QTextHtmlParserNode::WhiteSpacePre
1184 || wsm == QTextHtmlParserNode::WhiteSpaceNoWrap) {
1185 block.setNonBreakableLines(true);
1186 modifiedBlockFormat = true;
1187 }
1188
1189 if (currentNode->charFormat.background().style() != Qt::NoBrush && !currentNode->isTableCell()) {
1190 block.setBackground(currentNode->charFormat.background());
1191 modifiedBlockFormat = true;
1192 }
1193
1194 if (hasBlock && (!currentNode->isEmptyParagraph || forceBlockMerging)) {
1195 if (modifiedBlockFormat)
1196 cursor.setBlockFormat(block);
1197 if (modifiedCharFormat)
1198 cursor.setBlockCharFormat(charFmt);
1199 } else {
1200 if (currentNodeIdx == 1 && cursor.position() == 0 && currentNode->isEmptyParagraph) {
1201 cursor.setBlockFormat(block);
1202 cursor.setBlockCharFormat(charFmt);
1203 } else {
1204 appendBlock(block, charFmt);
1205 }
1206 }
1207
1208 if (currentNode->userState != -1)
1209 cursor.block().setUserState(currentNode->userState);
1210
1211 if (currentNode->id == Html_li && !lists.isEmpty()) {
1212 List &l = lists.last();
1213 if (l.list) {
1214 l.list->add(cursor.block());
1215 } else {
1216 l.list = cursor.createList(l.format);
1217 const qreal listTopMargin = topMargin(l.listNode);
1218 if (listTopMargin > block.topMargin()) {
1219 block.setTopMargin(listTopMargin);
1220 cursor.mergeBlockFormat(block);
1221 }
1222 }
1223 if (hasBlock) {
1224 QTextBlockFormat fmt;
1225 fmt.setIndent(currentNode->blockFormat.indent());
1226 cursor.mergeBlockFormat(fmt);
1227 }
1228 }
1229
1230 forceBlockMerging = false;
1231 if (currentNode->id == Html_body || currentNode->id == Html_html)
1232 forceBlockMerging = true;
1233
1234 if (currentNode->isEmptyParagraph) {
1235 hasBlock = false;
1236 return ContinueWithNextSibling;
1237 }
1238
1239 hasBlock = true;
1240 blockTagClosed = false;
1241 return ContinueWithCurrentNode;
1242}
1243
1244void QTextHtmlImporter::appendBlock(const QTextBlockFormat &format, QTextCharFormat charFmt)
1245{
1246 if (!namedAnchors.isEmpty()) {
1247 charFmt.setAnchor(true);
1248 charFmt.setAnchorNames(namedAnchors);
1249 namedAnchors.clear();
1250 }
1251
1252 cursor.insertBlock(format, charFmt);
1253
1254 if (wsm != QTextHtmlParserNode::WhiteSpacePre && wsm != QTextHtmlParserNode::WhiteSpacePreWrap)
1255 compressNextWhitespace = RemoveWhiteSpace;
1256}
1257
1258#endif // QT_NO_TEXTHTMLPARSER
1259
1260#ifndef QT_NO_TEXTHTMLPARSER
1261/*!
1262 \fn QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &text, const QTextDocument *resourceProvider)
1263 \since 4.2
1264
1265 Returns a QTextDocumentFragment based on the arbitrary piece of
1266 HTML in the given \a text. The formatting is preserved as much as
1267 possible; for example, "<b>bold</b>" will become a document
1268 fragment with the text "bold" with a bold character format.
1269
1270 If the provided HTML contains references to external resources such as imported style sheets, then
1271 they will be loaded through the \a resourceProvider.
1272*/
1273
1274QTextDocumentFragment QTextDocumentFragment::fromHtml(const QString &html, const QTextDocument *resourceProvider)
1275{
1276 QTextDocumentFragment res;
1277 res.d = new QTextDocumentFragmentPrivate;
1278
1279 QTextHtmlImporter importer(res.d->doc, html, QTextHtmlImporter::ImportToFragment, resourceProvider);
1280 importer.import();
1281 return res;
1282}
1283
1284QT_END_NAMESPACE
1285#endif // QT_NO_TEXTHTMLPARSER
1286