1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qtextdocument.h"
5#include <qtextformat.h>
6#include "qtextcursor_p.h"
7#include "qtextdocument_p.h"
8#include "qtextdocumentlayout_p.h"
9#include "qtextdocumentfragment.h"
10#include "qtextdocumentfragment_p.h"
11#include "qtexttable.h"
12#include "qtextlist.h"
13#include <qdebug.h>
14#include <qloggingcategory.h>
15#if QT_CONFIG(regularexpression)
16#include <qregularexpression.h>
17#endif
18#include <qvarlengtharray.h>
19#include <qthread.h>
20#include <qcoreapplication.h>
21#include <qmetaobject.h>
22
23#include "qtexthtmlparser_p.h"
24#include "qpainter.h"
25#include <qfile.h>
26#include <qfileinfo.h>
27#include <qdir.h>
28#include "qfont_p.h"
29#include "private/qdataurl_p.h"
30
31#include "qtextdocument_p.h"
32#include <private/qabstracttextdocumentlayout_p.h>
33#include "qpagedpaintdevice.h"
34#include "private/qpagedpaintdevice_p.h"
35#if QT_CONFIG(textmarkdownreader)
36#include <private/qtextmarkdownimporter_p.h>
37#endif
38#if QT_CONFIG(textmarkdownwriter)
39#include <private/qtextmarkdownwriter_p.h>
40#endif
41
42#include <limits.h>
43
44QT_BEGIN_NAMESPACE
45
46Q_DECLARE_LOGGING_CATEGORY(lcLayout);
47
48using namespace Qt::StringLiterals;
49
50Q_CORE_EXPORT Q_DECL_CONST_FUNCTION unsigned int qt_int_sqrt(unsigned int n);
51
52namespace {
53 QTextDocument::ResourceProvider qt_defaultResourceProvider;
54};
55
56QAbstractUndoItem::~QAbstractUndoItem()
57 = default;
58
59/*!
60 \fn bool Qt::mightBeRichText(QAnyStringView text)
61
62 Returns \c true if the string \a text is likely to be rich text;
63 otherwise returns \c false.
64
65 This function uses a fast and therefore simple heuristic. It
66 mainly checks whether there is something that looks like a tag
67 before the first line break. Although the result may be correct
68 for common cases, there is no guarantee.
69
70 This function is defined in the \c <QTextDocument> header file.
71
72 \note In Qt versions prior to 6.7, this function took QString only.
73 */
74template <typename T>
75static bool mightBeRichTextImpl(T text)
76{
77 if (text.isEmpty())
78 return false;
79 qsizetype start = 0;
80
81 while (start < text.size() && QChar(text.at(start)).isSpace())
82 ++start;
83
84 // skip a leading <?xml ... ?> as for example with xhtml
85 if (text.mid(start, 5).compare("<?xml"_L1) == 0) {
86 while (start < text.size()) {
87 if (text.at(start) == u'?'
88 && start + 2 < text.size()
89 && text.at(start + 1) == u'>') {
90 start += 2;
91 break;
92 }
93 ++start;
94 }
95
96 while (start < text.size() && QChar(text.at(start)).isSpace())
97 ++start;
98 }
99
100 if (text.mid(start, 5).compare("<!doc"_L1, Qt::CaseInsensitive) == 0)
101 return true;
102 qsizetype open = start;
103 while (open < text.size() && text.at(open) != u'<'
104 && text.at(open) != u'\n') {
105 if (text.at(open) == u'&' && text.mid(open + 1, 3) == "lt;"_L1)
106 return true; // support desperate attempt of user to see <...>
107 ++open;
108 }
109 if (open < text.size() && text.at(open) == u'<') {
110 const qsizetype close = text.indexOf(u'>', open);
111 if (close > -1) {
112 QVarLengthArray<char16_t> tag;
113 for (qsizetype i = open + 1; i < close; ++i) {
114 const auto current = QChar(text[i]);
115 if (current.isDigit() || current.isLetter())
116 tag.append(t: current.toLower().unicode());
117 else if (!tag.isEmpty() && current.isSpace())
118 break;
119 else if (!tag.isEmpty() && current == u'/' && i + 1 == close)
120 break;
121 else if (!current.isSpace() && (!tag.isEmpty() || current != u'!'))
122 return false; // that's not a tag
123 }
124#ifndef QT_NO_TEXTHTMLPARSER
125 return QTextHtmlParser::lookupElement(element: tag) != -1;
126#else
127 return false;
128#endif // QT_NO_TEXTHTMLPARSER
129 }
130 }
131 return false;
132}
133
134static bool mightBeRichTextImpl(QUtf8StringView text)
135{
136 return mightBeRichTextImpl(text: QLatin1StringView(QByteArrayView(text)));
137}
138
139bool Qt::mightBeRichText(QAnyStringView text)
140{
141 return text.visit(v: [](auto text) { return mightBeRichTextImpl(text); });
142}
143
144/*!
145 Converts the plain text string \a plain to an HTML-formatted
146 paragraph while preserving most of its look.
147
148 \a mode defines how whitespace is handled.
149
150 This function is defined in the \c <QTextDocument> header file.
151
152 \sa QString::toHtmlEscaped(), mightBeRichText()
153*/
154QString Qt::convertFromPlainText(const QString &plain, Qt::WhiteSpaceMode mode)
155{
156 qsizetype col = 0;
157 QString rich;
158 rich += "<p>"_L1;
159 for (qsizetype i = 0; i < plain.size(); ++i) {
160 if (plain[i] == u'\n'){
161 qsizetype c = 1;
162 while (i+1 < plain.size() && plain[i+1] == u'\n') {
163 i++;
164 c++;
165 }
166 if (c == 1)
167 rich += "<br>\n"_L1;
168 else {
169 rich += "</p>\n"_L1;
170 while (--c > 1)
171 rich += "<br>\n"_L1;
172 rich += "<p>"_L1;
173 }
174 col = 0;
175 } else {
176 if (mode == Qt::WhiteSpacePre && plain[i] == u'\t'){
177 rich += QChar::Nbsp;
178 ++col;
179 while (col % 8) {
180 rich += QChar::Nbsp;
181 ++col;
182 }
183 }
184 else if (mode == Qt::WhiteSpacePre && plain[i].isSpace())
185 rich += QChar::Nbsp;
186 else if (plain[i] == u'<')
187 rich += "&lt;"_L1;
188 else if (plain[i] == u'>')
189 rich += "&gt;"_L1;
190 else if (plain[i] == u'&')
191 rich += "&amp;"_L1;
192 else
193 rich += plain[i];
194 ++col;
195 }
196 }
197 if (col != 0)
198 rich += "</p>"_L1;
199 return rich;
200}
201
202/*!
203 \class QTextDocument
204 \reentrant
205 \inmodule QtGui
206
207 \brief The QTextDocument class holds formatted text.
208
209 \ingroup richtext-processing
210
211
212 QTextDocument is a container for structured rich text documents, providing
213 support for styled text and various types of document elements, such as
214 lists, tables, frames, and images.
215 They can be created for use in a QTextEdit, or used independently.
216
217 Each document element is described by an associated format object. Each
218 format object is treated as a unique object by QTextDocuments, and can be
219 passed to objectForFormat() to obtain the document element that it is
220 applied to.
221
222 A QTextDocument can be edited programmatically using a QTextCursor, and
223 its contents can be examined by traversing the document structure. The
224 entire document structure is stored as a hierarchy of document elements
225 beneath the root frame, found with the rootFrame() function. Alternatively,
226 if you just want to iterate over the textual contents of the document you
227 can use begin(), end(), and findBlock() to retrieve text blocks that you
228 can examine and iterate over.
229
230 The layout of a document is determined by the documentLayout();
231 you can create your own QAbstractTextDocumentLayout subclass and
232 set it using setDocumentLayout() if you want to use your own
233 layout logic. The document's title and other meta-information can be
234 obtained by calling the metaInformation() function. For documents that
235 are exposed to users through the QTextEdit class, the document title
236 is also available via the QTextEdit::documentTitle() function.
237
238 The toPlainText() and toHtml() convenience functions allow you to retrieve the
239 contents of the document as plain text and HTML.
240 The document's text can be searched using the find() functions.
241
242 Undo/redo of operations performed on the document can be controlled using
243 the setUndoRedoEnabled() function. The undo/redo system can be controlled
244 by an editor widget through the undo() and redo() slots; the document also
245 provides contentsChanged(), undoAvailable(), and redoAvailable() signals
246 that inform connected editor widgets about the state of the undo/redo
247 system. The following are the undo/redo operations of a QTextDocument:
248
249 \list
250 \li Insertion or removal of characters. A sequence of insertions or removals
251 within the same text block are regarded as a single undo/redo operation.
252 \li Insertion or removal of text blocks. Sequences of insertion or removals
253 in a single operation (e.g., by selecting and then deleting text) are
254 regarded as a single undo/redo operation.
255 \li Text character format changes.
256 \li Text block format changes.
257 \li Text block group format changes.
258 \endlist
259
260 \sa QTextCursor, QTextEdit, {Rich Text Processing}
261*/
262
263/*!
264 \property QTextDocument::defaultFont
265 \brief the default font used to display the document's text
266*/
267
268/*!
269 \property QTextDocument::defaultTextOption
270 \brief the default text option will be set on all \l{QTextLayout}s in the document.
271
272 When \l{QTextBlock}s are created, the defaultTextOption is set on their
273 QTextLayout. This allows setting global properties for the document such as the
274 default word wrap mode.
275 */
276
277/*!
278 Constructs an empty QTextDocument with the given \a parent.
279*/
280QTextDocument::QTextDocument(QObject *parent)
281 : QObject(*new QTextDocumentPrivate, parent)
282{
283 Q_D(QTextDocument);
284 d->init();
285}
286
287/*!
288 Constructs a QTextDocument containing the plain (unformatted) \a text
289 specified, and with the given \a parent.
290*/
291QTextDocument::QTextDocument(const QString &text, QObject *parent)
292 : QObject(*new QTextDocumentPrivate, parent)
293{
294 Q_D(QTextDocument);
295 d->init();
296 QTextCursor(this).insertText(text);
297}
298
299/*!
300 \internal
301*/
302QTextDocument::QTextDocument(QTextDocumentPrivate &dd, QObject *parent)
303 : QObject(dd, parent)
304{
305 Q_D(QTextDocument);
306 d->init();
307}
308
309/*!
310 Destroys the document.
311*/
312QTextDocument::~QTextDocument()
313{
314}
315
316
317/*!
318 Creates a new QTextDocument that is a copy of this text document. \a
319 parent is the parent of the returned text document.
320*/
321QTextDocument *QTextDocument::clone(QObject *parent) const
322{
323 Q_D(const QTextDocument);
324 QTextDocument *doc = new QTextDocument(parent);
325 if (isEmpty()) {
326 const QTextCursor thisCursor(const_cast<QTextDocument *>(this));
327
328 const auto blockFormat = thisCursor.blockFormat();
329 if (blockFormat.isValid() && !blockFormat.isEmpty())
330 QTextCursor(doc).setBlockFormat(blockFormat);
331
332 const auto blockCharFormat = thisCursor.blockCharFormat();
333 if (blockCharFormat.isValid() && !blockCharFormat.isEmpty())
334 QTextCursor(doc).setBlockCharFormat(blockCharFormat);
335 } else {
336 QTextCursor(doc).insertFragment(fragment: QTextDocumentFragment(this));
337 }
338 doc->rootFrame()->setFrameFormat(rootFrame()->frameFormat());
339 QTextDocumentPrivate *priv = doc->d_func();
340 priv->title = d->title;
341 priv->url = d->url;
342 priv->cssMedia = d->cssMedia;
343 priv->pageSize = d->pageSize;
344 priv->indentWidth = d->indentWidth;
345 priv->defaultTextOption = d->defaultTextOption;
346 priv->setDefaultFont(d->defaultFont());
347 priv->resources = d->resources;
348 priv->cachedResources.clear();
349 priv->resourceProvider = d->resourceProvider;
350#ifndef QT_NO_CSSPARSER
351 priv->defaultStyleSheet = d->defaultStyleSheet;
352 priv->parsedDefaultStyleSheet = d->parsedDefaultStyleSheet;
353#endif
354 return doc;
355}
356
357/*!
358 Returns \c true if the document is empty; otherwise returns \c false.
359*/
360bool QTextDocument::isEmpty() const
361{
362 Q_D(const QTextDocument);
363 /* because if we're empty we still have one single paragraph as
364 * one single fragment */
365 return d->length() <= 1;
366}
367
368/*!
369 Clears the document.
370*/
371void QTextDocument::clear()
372{
373 Q_D(QTextDocument);
374 d->clear();
375 d->resources.clear();
376}
377
378/*!
379 \since 4.2
380
381 Undoes the last editing operation on the document if undo is
382 available. The provided \a cursor is positioned at the end of the
383 location where the edition operation was undone.
384
385 See the \l {Overview of Qt's Undo Framework}{Qt Undo Framework}
386 documentation for details.
387
388 \sa undoAvailable(), isUndoRedoEnabled()
389*/
390void QTextDocument::undo(QTextCursor *cursor)
391{
392 Q_D(QTextDocument);
393 const int pos = d->undoRedo(undo: true);
394 if (cursor && pos >= 0) {
395 *cursor = QTextCursor(this);
396 cursor->setPosition(pos);
397 }
398}
399
400/*!
401 \since 4.2
402 Redoes the last editing operation on the document if \l{QTextDocument::isRedoAvailable()}{redo is available}.
403
404 The provided \a cursor is positioned at the end of the location where
405 the edition operation was redone.
406*/
407void QTextDocument::redo(QTextCursor *cursor)
408{
409 Q_D(QTextDocument);
410 const int pos = d->undoRedo(undo: false);
411 if (cursor && pos >= 0) {
412 *cursor = QTextCursor(this);
413 cursor->setPosition(pos);
414 }
415}
416
417/*! \enum QTextDocument::Stacks
418
419 \value UndoStack The undo stack.
420 \value RedoStack The redo stack.
421 \value UndoAndRedoStacks Both the undo and redo stacks.
422*/
423
424/*!
425 \since 4.7
426 Clears the stacks specified by \a stacksToClear.
427
428 This method clears any commands on the undo stack, the redo stack,
429 or both (the default). If commands are cleared, the appropriate
430 signals are emitted, QTextDocument::undoAvailable() or
431 QTextDocument::redoAvailable().
432
433 \sa QTextDocument::undoAvailable(), QTextDocument::redoAvailable()
434*/
435void QTextDocument::clearUndoRedoStacks(Stacks stacksToClear)
436{
437 Q_D(QTextDocument);
438 d->clearUndoRedoStacks(stacksToClear, emitSignals: true);
439}
440
441/*!
442 \overload
443
444*/
445void QTextDocument::undo()
446{
447 Q_D(QTextDocument);
448 d->undoRedo(undo: true);
449}
450
451/*!
452 \overload
453 Redoes the last editing operation on the document if \l{QTextDocument::isRedoAvailable()}{redo is available}.
454*/
455void QTextDocument::redo()
456{
457 Q_D(QTextDocument);
458 d->undoRedo(undo: false);
459}
460
461/*!
462 \internal
463
464 Appends a custom undo \a item to the undo stack.
465*/
466void QTextDocument::appendUndoItem(QAbstractUndoItem *item)
467{
468 Q_D(QTextDocument);
469 d->appendUndoItem(item);
470}
471
472/*!
473 \property QTextDocument::undoRedoEnabled
474 \brief whether undo/redo are enabled for this document
475
476 This defaults to true. If disabled, the undo stack is cleared and
477 no items will be added to it.
478*/
479void QTextDocument::setUndoRedoEnabled(bool enable)
480{
481 Q_D(QTextDocument);
482 d->enableUndoRedo(enable);
483}
484
485bool QTextDocument::isUndoRedoEnabled() const
486{
487 Q_D(const QTextDocument);
488 return d->isUndoRedoEnabled();
489}
490
491/*!
492 \property QTextDocument::maximumBlockCount
493 \since 4.2
494 \brief Specifies the limit for blocks in the document.
495
496 Specifies the maximum number of blocks the document may have. If there are
497 more blocks in the document that specified with this property blocks are removed
498 from the beginning of the document.
499
500 A negative or zero value specifies that the document may contain an unlimited
501 amount of blocks.
502
503 The default value is 0.
504
505 Note that setting this property will apply the limit immediately to the document
506 contents.
507
508 Setting this property also disables the undo redo history.
509
510 This property is undefined in documents with tables or frames.
511*/
512int QTextDocument::maximumBlockCount() const
513{
514 Q_D(const QTextDocument);
515 return d->maximumBlockCount;
516}
517
518void QTextDocument::setMaximumBlockCount(int maximum)
519{
520 Q_D(QTextDocument);
521 d->maximumBlockCount = maximum;
522 d->ensureMaximumBlockCount();
523 setUndoRedoEnabled(false);
524}
525
526/*!
527 \since 4.3
528
529 The default text option is used on all QTextLayout objects in the document.
530 This allows setting global properties for the document such as the default
531 word wrap mode.
532*/
533QTextOption QTextDocument::defaultTextOption() const
534{
535 Q_D(const QTextDocument);
536 return d->defaultTextOption;
537}
538
539/*!
540 \since 4.3
541
542 Sets the default text option to \a option.
543*/
544void QTextDocument::setDefaultTextOption(const QTextOption &option)
545{
546 Q_D(QTextDocument);
547 d->defaultTextOption = option;
548 if (d->lout)
549 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
550}
551
552/*!
553 \property QTextDocument::baseUrl
554 \since 5.3
555 \brief the base URL used to resolve relative resource URLs within the document.
556
557 Resource URLs are resolved to be within the same directory as the target of the base
558 URL meaning any portion of the path after the last '/' will be ignored.
559
560 \table
561 \header \li Base URL \li Relative URL \li Resolved URL
562 \row \li file:///path/to/content \li images/logo.png \li file:///path/to/images/logo.png
563 \row \li file:///path/to/content/ \li images/logo.png \li file:///path/to/content/images/logo.png
564 \row \li file:///path/to/content/index.html \li images/logo.png \li file:///path/to/content/images/logo.png
565 \row \li file:///path/to/content/images/ \li ../images/logo.png \li file:///path/to/content/images/logo.png
566 \endtable
567*/
568QUrl QTextDocument::baseUrl() const
569{
570 Q_D(const QTextDocument);
571 return d->baseUrl;
572}
573
574void QTextDocument::setBaseUrl(const QUrl &url)
575{
576 Q_D(QTextDocument);
577 if (d->baseUrl != url) {
578 d->baseUrl = url;
579 if (d->lout)
580 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
581 emit baseUrlChanged(url);
582 }
583}
584
585/*!
586 \since 4.8
587
588 The default cursor movement style is used by all QTextCursor objects
589 created from the document. The default is Qt::LogicalMoveStyle.
590*/
591Qt::CursorMoveStyle QTextDocument::defaultCursorMoveStyle() const
592{
593 Q_D(const QTextDocument);
594 return d->defaultCursorMoveStyle;
595}
596
597/*!
598 \since 4.8
599
600 Sets the default cursor movement style to the given \a style.
601*/
602void QTextDocument::setDefaultCursorMoveStyle(Qt::CursorMoveStyle style)
603{
604 Q_D(QTextDocument);
605 d->defaultCursorMoveStyle = style;
606}
607
608/*!
609 \fn void QTextDocument::markContentsDirty(int position, int length)
610
611 Marks the contents specified by the given \a position and \a length
612 as "dirty", informing the document that it needs to be laid out
613 again.
614*/
615void QTextDocument::markContentsDirty(int from, int length)
616{
617 Q_D(QTextDocument);
618 d->documentChange(from, length);
619 if (!d->inContentsChange) {
620 if (d->lout) {
621 d->lout->documentChanged(from: d->docChangeFrom, charsRemoved: d->docChangeOldLength, charsAdded: d->docChangeLength);
622 d->docChangeFrom = -1;
623 }
624 }
625}
626
627/*!
628 \property QTextDocument::useDesignMetrics
629 \since 4.1
630 \brief whether the document uses design metrics of fonts to improve the accuracy of text layout
631
632 If this property is set to true, the layout will use design metrics.
633 Otherwise, the metrics of the paint device as set on
634 QAbstractTextDocumentLayout::setPaintDevice() will be used.
635
636 Using design metrics makes a layout have a width that is no longer dependent on hinting
637 and pixel-rounding. This means that WYSIWYG text layout becomes possible because the width
638 scales much more linearly based on paintdevice metrics than it would otherwise.
639
640 By default, this property is \c false.
641*/
642
643void QTextDocument::setUseDesignMetrics(bool b)
644{
645 Q_D(QTextDocument);
646 if (b == d->defaultTextOption.useDesignMetrics())
647 return;
648 d->defaultTextOption.setUseDesignMetrics(b);
649 if (d->lout)
650 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
651}
652
653bool QTextDocument::useDesignMetrics() const
654{
655 Q_D(const QTextDocument);
656 return d->defaultTextOption.useDesignMetrics();
657}
658
659/*!
660 \property QTextDocument::layoutEnabled
661 \since 6.4
662 \brief whether QTextDocument should recalculate the layout after every change
663
664 If this property is set to true, any change to the document triggers a layout,
665 which makes everything work as expected but takes time.
666
667 Temporarily disabling the layout can save time when making multiple changes
668 (not just text content, but also default font, default text option....)
669 so that the document is only laid out once at the end. This can be useful when
670 the text width or page size isn't yet known, for instance.
671
672 By default, this property is \c true.
673
674 \sa setTextWidth
675*/
676
677void QTextDocument::setLayoutEnabled(bool b)
678{
679 Q_D(QTextDocument);
680 if (d->layoutEnabled == b)
681 return;
682 d->layoutEnabled = b;
683 if (b && d->lout)
684 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
685}
686
687bool QTextDocument::isLayoutEnabled() const
688{
689 Q_D(const QTextDocument);
690 return d->layoutEnabled;
691}
692
693/*!
694 \since 4.2
695
696 Draws the content of the document with painter \a p, clipped to \a rect.
697 If \a rect is a null rectangle (default) then the document is painted unclipped.
698*/
699void QTextDocument::drawContents(QPainter *p, const QRectF &rect)
700{
701 p->save();
702 QAbstractTextDocumentLayout::PaintContext ctx;
703 if (rect.isValid()) {
704 p->setClipRect(rect);
705 ctx.clip = rect;
706 }
707 documentLayout()->draw(painter: p, context: ctx);
708 p->restore();
709}
710
711/*!
712 \property QTextDocument::textWidth
713 \since 4.2
714
715 The text width specifies the preferred width for text in the document. If
716 the text (or content in general) is wider than the specified with it is broken
717 into multiple lines and grows vertically. If the text cannot be broken into multiple
718 lines to fit into the specified text width it will be larger and the size() and the
719 idealWidth() property will reflect that.
720
721 If the text width is set to -1 then the text will not be broken into multiple lines
722 unless it is enforced through an explicit line break or a new paragraph.
723
724 The default value is -1.
725
726 Setting the text width will also set the page height to -1, causing the document to
727 grow or shrink vertically in a continuous way. If you want the document layout to break
728 the text into multiple pages then you have to set the pageSize property instead.
729
730 \sa size(), idealWidth(), pageSize()
731*/
732void QTextDocument::setTextWidth(qreal width)
733{
734 Q_D(QTextDocument);
735 QSizeF sz = d->pageSize;
736
737 qCDebug(lcLayout) << "page size" << sz << "-> width" << width;
738 sz.setWidth(width);
739 sz.setHeight(-1);
740 setPageSize(sz);
741}
742
743qreal QTextDocument::textWidth() const
744{
745 Q_D(const QTextDocument);
746 return d->pageSize.width();
747}
748
749/*!
750 \since 4.2
751
752 Returns the ideal width of the text document. The ideal width is the actually used width
753 of the document without optional alignments taken into account. It is always <= size().width().
754
755 \sa adjustSize(), textWidth
756*/
757qreal QTextDocument::idealWidth() const
758{
759 if (QTextDocumentLayout *lout = qobject_cast<QTextDocumentLayout *>(object: documentLayout()))
760 return lout->idealWidth();
761 return textWidth();
762}
763
764/*!
765 \property QTextDocument::documentMargin
766 \since 4.5
767
768 The margin around the document. The default is 4.
769*/
770qreal QTextDocument::documentMargin() const
771{
772 Q_D(const QTextDocument);
773 return d->documentMargin;
774}
775
776void QTextDocument::setDocumentMargin(qreal margin)
777{
778 Q_D(QTextDocument);
779 if (d->documentMargin != margin) {
780 d->documentMargin = margin;
781
782 QTextFrame* root = rootFrame();
783 QTextFrameFormat format = root->frameFormat();
784 format.setMargin(margin);
785 root->setFrameFormat(format);
786
787 if (d->lout)
788 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
789 }
790}
791
792
793/*!
794 \property QTextDocument::indentWidth
795 \since 4.4
796
797 Returns the width used for text list and text block indenting.
798
799 The indent properties of QTextListFormat and QTextBlockFormat specify
800 multiples of this value. The default indent width is 40.
801*/
802qreal QTextDocument::indentWidth() const
803{
804 Q_D(const QTextDocument);
805 return d->indentWidth;
806}
807
808
809/*!
810 \since 4.4
811
812 Sets the \a width used for text list and text block indenting.
813
814 The indent properties of QTextListFormat and QTextBlockFormat specify
815 multiples of this value. The default indent width is 40 .
816
817 \sa indentWidth()
818*/
819void QTextDocument::setIndentWidth(qreal width)
820{
821 Q_D(QTextDocument);
822 if (d->indentWidth != width) {
823 d->indentWidth = width;
824 if (d->lout)
825 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
826 }
827}
828
829
830
831
832/*!
833 \since 4.2
834
835 Adjusts the document to a reasonable size.
836
837 \sa idealWidth(), textWidth, size
838*/
839void QTextDocument::adjustSize()
840{
841 // Pull this private function in from qglobal.cpp
842 QFont f = defaultFont();
843 QFontMetrics fm(f);
844 int mw = fm.horizontalAdvance(u'x') * 80;
845 int w = mw;
846 setTextWidth(w);
847 QSizeF size = documentLayout()->documentSize();
848 if (size.width() != 0) {
849 w = qt_int_sqrt(n: (uint)(5 * size.height() * size.width() / 3));
850 setTextWidth(qMin(a: w, b: mw));
851
852 size = documentLayout()->documentSize();
853 if (w*3 < 5*size.height()) {
854 w = qt_int_sqrt(n: (uint)(2 * size.height() * size.width()));
855 setTextWidth(qMin(a: w, b: mw));
856 }
857 }
858 setTextWidth(idealWidth());
859}
860
861/*!
862 \property QTextDocument::size
863 \since 4.2
864
865 \brief the actual size of the document.
866 This is equivalent to documentLayout()->documentSize();
867
868 The size of the document can be changed either by setting
869 a text width or setting an entire page size.
870
871 Note that the width is always >= pageSize().width().
872
873 By default, for a newly-created, empty document, this property contains
874 a configuration-dependent size.
875
876 \sa setTextWidth(), setPageSize(), idealWidth()
877*/
878QSizeF QTextDocument::size() const
879{
880 return documentLayout()->documentSize();
881}
882
883/*!
884 \property QTextDocument::blockCount
885 \since 4.2
886
887 \brief the number of text blocks in the document.
888
889 The value of this property is undefined in documents with tables or frames.
890
891 By default, if defined, this property contains a value of 1.
892 \sa lineCount(), characterCount()
893*/
894int QTextDocument::blockCount() const
895{
896 Q_D(const QTextDocument);
897 return d->blockMap().numNodes();
898}
899
900
901/*!
902 \since 4.5
903
904 Returns the number of lines of this document (if the layout supports
905 this). Otherwise, this is identical to the number of blocks.
906
907 \sa blockCount(), characterCount()
908 */
909int QTextDocument::lineCount() const
910{
911 Q_D(const QTextDocument);
912 return d->blockMap().length(field: 2);
913}
914
915/*!
916 \since 4.5
917
918 Returns the number of characters of this document.
919
920 \note As a QTextDocument always contains at least one
921 QChar::ParagraphSeparator, this method will return at least 1.
922
923 \sa blockCount(), characterAt()
924 */
925int QTextDocument::characterCount() const
926{
927 Q_D(const QTextDocument);
928 return d->length();
929}
930
931/*!
932 \since 4.5
933
934 Returns the character at position \a pos, or a null character if the
935 position is out of range.
936
937 \sa characterCount()
938 */
939QChar QTextDocument::characterAt(int pos) const
940{
941 Q_D(const QTextDocument);
942 if (pos < 0 || pos >= d->length())
943 return QChar();
944 QTextDocumentPrivate::FragmentIterator fragIt = d->find(pos);
945 const QTextFragmentData * const frag = fragIt.value();
946 const int offsetInFragment = qMax(a: 0, b: pos - fragIt.position());
947 return d->text.at(i: frag->stringPosition + offsetInFragment);
948}
949
950
951/*!
952 \property QTextDocument::defaultStyleSheet
953 \since 4.2
954
955 The default style sheet is applied to all newly HTML formatted text that is
956 inserted into the document, for example using setHtml() or QTextCursor::insertHtml().
957
958 The style sheet needs to be compliant to CSS 2.1 syntax.
959
960 \b{Note:} Changing the default style sheet does not have any effect to the existing content
961 of the document.
962
963 \sa {Supported HTML Subset}
964*/
965
966#ifndef QT_NO_CSSPARSER
967void QTextDocument::setDefaultStyleSheet(const QString &sheet)
968{
969 Q_D(QTextDocument);
970 d->defaultStyleSheet = sheet;
971 QCss::Parser parser(sheet);
972 d->parsedDefaultStyleSheet = QCss::StyleSheet();
973 d->parsedDefaultStyleSheet.origin = QCss::StyleSheetOrigin_UserAgent;
974 parser.parse(styleSheet: &d->parsedDefaultStyleSheet);
975}
976
977QString QTextDocument::defaultStyleSheet() const
978{
979 Q_D(const QTextDocument);
980 return d->defaultStyleSheet;
981}
982#endif // QT_NO_CSSPARSER
983
984/*!
985 \fn void QTextDocument::contentsChanged()
986
987 This signal is emitted whenever the document's content changes; for
988 example, when text is inserted or deleted, or when formatting is applied.
989
990 \sa contentsChange()
991*/
992
993/*!
994 \fn void QTextDocument::contentsChange(int position, int charsRemoved, int charsAdded)
995
996 This signal is emitted whenever the document's content changes; for
997 example, when text is inserted or deleted, or when formatting is applied.
998
999 Information is provided about the \a position of the character in the
1000 document where the change occurred, the number of characters removed
1001 (\a charsRemoved), and the number of characters added (\a charsAdded).
1002
1003 The signal is emitted before the document's layout manager is notified
1004 about the change. This hook allows you to implement syntax highlighting
1005 for the document.
1006
1007 \sa QAbstractTextDocumentLayout::documentChanged(), contentsChanged()
1008*/
1009
1010
1011/*!
1012 \fn void QTextDocument::undoAvailable(bool available);
1013
1014 This signal is emitted whenever undo operations become available
1015 (\a available is true) or unavailable (\a available is false).
1016
1017 See the \l {Overview of Qt's Undo Framework}{Qt Undo Framework}
1018 documentation for details.
1019
1020 \sa undo(), isUndoRedoEnabled()
1021*/
1022
1023/*!
1024 \fn void QTextDocument::redoAvailable(bool available);
1025
1026 This signal is emitted whenever redo operations become available
1027 (\a available is true) or unavailable (\a available is false).
1028*/
1029
1030/*!
1031 \fn void QTextDocument::cursorPositionChanged(const QTextCursor &cursor);
1032
1033 This signal is emitted whenever the position of a cursor changed
1034 due to an editing operation. The cursor that changed is passed in
1035 \a cursor. If the document is used with the QTextEdit class and you need a signal when the
1036 cursor is moved with the arrow keys you can use the \l{QTextEdit::}{cursorPositionChanged()}
1037 signal in QTextEdit.
1038*/
1039
1040/*!
1041 \fn void QTextDocument::blockCountChanged(int newBlockCount);
1042 \since 4.3
1043
1044 This signal is emitted when the total number of text blocks in the
1045 document changes. The value passed in \a newBlockCount is the new
1046 total.
1047*/
1048
1049/*!
1050 \fn void QTextDocument::documentLayoutChanged();
1051 \since 4.4
1052
1053 This signal is emitted when a new document layout is set.
1054
1055 \sa setDocumentLayout()
1056
1057*/
1058
1059
1060/*!
1061 Returns \c true if undo is available; otherwise returns \c false.
1062
1063 \sa isRedoAvailable(), availableUndoSteps()
1064*/
1065bool QTextDocument::isUndoAvailable() const
1066{
1067 Q_D(const QTextDocument);
1068 return d->isUndoAvailable();
1069}
1070
1071/*!
1072 Returns \c true if redo is available; otherwise returns \c false.
1073
1074 \sa isUndoAvailable(), availableRedoSteps()
1075*/
1076bool QTextDocument::isRedoAvailable() const
1077{
1078 Q_D(const QTextDocument);
1079 return d->isRedoAvailable();
1080}
1081
1082/*! \since 4.6
1083
1084 Returns the number of available undo steps.
1085
1086 \sa isUndoAvailable()
1087*/
1088int QTextDocument::availableUndoSteps() const
1089{
1090 Q_D(const QTextDocument);
1091 return d->availableUndoSteps();
1092}
1093
1094/*! \since 4.6
1095
1096 Returns the number of available redo steps.
1097
1098 \sa isRedoAvailable()
1099*/
1100int QTextDocument::availableRedoSteps() const
1101{
1102 Q_D(const QTextDocument);
1103 return d->availableRedoSteps();
1104}
1105
1106/*! \since 4.4
1107
1108 Returns the document's revision (if undo is enabled).
1109
1110 The revision is guaranteed to increase when a document that is not
1111 modified is edited.
1112
1113 \sa QTextBlock::revision(), isModified()
1114 */
1115int QTextDocument::revision() const
1116{
1117 Q_D(const QTextDocument);
1118 return d->revision;
1119}
1120
1121
1122
1123/*!
1124 Sets the document to use the given \a layout. The previous layout
1125 is deleted.
1126
1127 \sa documentLayoutChanged()
1128*/
1129void QTextDocument::setDocumentLayout(QAbstractTextDocumentLayout *layout)
1130{
1131 Q_D(QTextDocument);
1132 d->setLayout(layout);
1133}
1134
1135/*!
1136 Returns the document layout for this document.
1137*/
1138QAbstractTextDocumentLayout *QTextDocument::documentLayout() const
1139{
1140 Q_D(const QTextDocument);
1141 if (!d->lout) {
1142 QTextDocument *that = const_cast<QTextDocument *>(this);
1143 that->d_func()->setLayout(new QTextDocumentLayout(that));
1144 }
1145 return d->lout;
1146}
1147
1148
1149/*!
1150 Returns meta information about the document of the type specified by
1151 \a info.
1152
1153 \sa setMetaInformation()
1154*/
1155QString QTextDocument::metaInformation(MetaInformation info) const
1156{
1157 Q_D(const QTextDocument);
1158 switch (info) {
1159 case DocumentTitle:
1160 return d->title;
1161 case DocumentUrl:
1162 return d->url;
1163 case CssMedia:
1164 return d->cssMedia;
1165 case FrontMatter:
1166 return d->frontMatter;
1167 }
1168 return QString();
1169}
1170
1171/*!
1172 Sets the document's meta information of the type specified by \a info
1173 to the given \a string.
1174
1175 \sa metaInformation()
1176*/
1177void QTextDocument::setMetaInformation(MetaInformation info, const QString &string)
1178{
1179 Q_D(QTextDocument);
1180 switch (info) {
1181 case DocumentTitle:
1182 d->title = string;
1183 break;
1184 case DocumentUrl:
1185 d->url = string;
1186 break;
1187 case CssMedia:
1188 d->cssMedia = string;
1189 break;
1190 case FrontMatter:
1191 d->frontMatter = string;
1192 break;
1193 }
1194}
1195
1196/*!
1197 Returns the raw text contained in the document without any
1198 formatting information. If you want formatting information
1199 use a QTextCursor instead.
1200
1201 \since 5.9
1202 \sa toPlainText()
1203*/
1204QString QTextDocument::toRawText() const
1205{
1206 Q_D(const QTextDocument);
1207 return d->plainText();
1208}
1209
1210/*!
1211 Returns the plain text contained in the document. If you want
1212 formatting information use a QTextCursor instead.
1213
1214 This function returns the same as toRawText(), but will replace
1215 some unicode characters with ASCII alternatives.
1216 In particular, no-break space (U+00A0) is replaced by a regular
1217 space (U+0020), and both paragraph (U+2029) and line (U+2028)
1218 separators are replaced by line feed (U+000A).
1219 If you need the precise contents of the document, use toRawText()
1220 instead.
1221
1222 \note Embedded objects, such as images, are represented by a
1223 Unicode value U+FFFC (OBJECT REPLACEMENT CHARACTER).
1224
1225 \sa toHtml()
1226*/
1227QString QTextDocument::toPlainText() const
1228{
1229 Q_D(const QTextDocument);
1230 QString txt = d->plainText();
1231
1232 constexpr char16_t delims[] = { 0xfdd0, 0xfdd1,
1233 QChar::ParagraphSeparator, QChar::LineSeparator, QChar::Nbsp };
1234
1235 const size_t pos = std::u16string_view(txt).find_first_of(
1236 str: std::u16string_view(delims, std::size(delims)));
1237 if (pos == std::u16string_view::npos)
1238 return txt;
1239
1240 QChar *uc = txt.data();
1241 QChar *const e = uc + txt.size();
1242
1243 for (uc += pos; uc != e; ++uc) {
1244 switch (uc->unicode()) {
1245 case 0xfdd0: // QTextBeginningOfFrame
1246 case 0xfdd1: // QTextEndOfFrame
1247 case QChar::ParagraphSeparator:
1248 case QChar::LineSeparator:
1249 *uc = u'\n';
1250 break;
1251 case QChar::Nbsp:
1252 *uc = u' ';
1253 break;
1254 default:
1255 ;
1256 }
1257 }
1258 return txt;
1259}
1260
1261/*!
1262 Replaces the entire contents of the document with the given plain
1263 \a text. The undo/redo history is reset when this function is called.
1264
1265 \sa setHtml()
1266*/
1267void QTextDocument::setPlainText(const QString &text)
1268{
1269 Q_D(QTextDocument);
1270 bool previousState = d->isUndoRedoEnabled();
1271 d->enableUndoRedo(enable: false);
1272 d->beginEditBlock();
1273 d->clear();
1274 QTextCursor(this).insertText(text);
1275 d->endEditBlock();
1276 d->enableUndoRedo(enable: previousState);
1277}
1278
1279/*!
1280 Replaces the entire contents of the document with the given
1281 HTML-formatted text in the \a html string. The undo/redo history
1282 is reset when this function is called.
1283
1284 The HTML formatting is respected as much as possible; for example,
1285 "<b>bold</b> text" will produce text where the first word has a font
1286 weight that gives it a bold appearance: "\b{bold} text".
1287
1288 To select a css media rule other than the default "screen" rule,
1289 use setMetaInformation() with 'CssMedia' as "info" parameter.
1290
1291 \note It is the responsibility of the caller to make sure that the
1292 text is correctly decoded when a QString containing HTML is created
1293 and passed to setHtml().
1294
1295 \sa setPlainText(), {Supported HTML Subset}, setMetaInformation()
1296*/
1297
1298#ifndef QT_NO_TEXTHTMLPARSER
1299
1300void QTextDocument::setHtml(const QString &html)
1301{
1302 Q_D(QTextDocument);
1303 bool previousState = d->isUndoRedoEnabled();
1304 d->enableUndoRedo(enable: false);
1305 d->beginEditBlock();
1306 d->clear();
1307 // ctor calls parse() to build up QTextHtmlParser::nodes list
1308 // then import() populates the QTextDocument from those
1309 QTextHtmlImporter(this, html, QTextHtmlImporter::ImportToDocument).import();
1310 d->endEditBlock();
1311 d->enableUndoRedo(enable: previousState);
1312}
1313
1314#endif // QT_NO_TEXTHTMLPARSER
1315
1316/*!
1317 \enum QTextDocument::FindFlag
1318
1319 This enum describes the options available to QTextDocument's find function. The options
1320 can be OR-ed together from the following list:
1321
1322 \value FindBackward Search backwards instead of forwards.
1323 \value FindCaseSensitively By default find works case insensitive. Specifying this option
1324 changes the behaviour to a case sensitive find operation.
1325 \value FindWholeWords Makes find match only complete words.
1326*/
1327
1328/*!
1329 \enum QTextDocument::MetaInformation
1330
1331 This enum describes the different types of meta information that can be
1332 added to a document.
1333
1334 \value DocumentTitle The title of the document.
1335 \value DocumentUrl The url of the document. The loadResource() function uses
1336 this url as the base when loading relative resources.
1337 \value CssMedia This value is used to select the corresponding '@media'
1338 rule, if any, from a specified CSS stylesheet when setHtml()
1339 is called. This enum value has been introduced in Qt 6.3.
1340 \value FrontMatter This value is used to select header material, if any was
1341 extracted during parsing of the source file (currently
1342 only from Markdown format). This enum value has been
1343 introduced in Qt 6.8.
1344
1345 \sa metaInformation(), setMetaInformation(), setHtml()
1346*/
1347
1348static bool findInBlock(const QTextBlock &block, const QString &expression, int offset,
1349 QTextDocument::FindFlags options, QTextCursor *cursor)
1350{
1351 QString text = block.text();
1352 text.replace(before: QChar::Nbsp, after: u' ');
1353 Qt::CaseSensitivity sensitivity = options & QTextDocument::FindCaseSensitively ? Qt::CaseSensitive : Qt::CaseInsensitive;
1354 int idx = -1;
1355
1356 while (offset >= 0 && offset <= text.size()) {
1357 idx = (options & QTextDocument::FindBackward) ?
1358 text.lastIndexOf(s: expression, from: offset, cs: sensitivity) : text.indexOf(s: expression, from: offset, cs: sensitivity);
1359 if (idx == -1)
1360 return false;
1361
1362 if (options & QTextDocument::FindWholeWords) {
1363 const int start = idx;
1364 const int end = start + expression.size();
1365 if ((start != 0 && text.at(i: start - 1).isLetterOrNumber())
1366 || (end != text.size() && text.at(i: end).isLetterOrNumber())) {
1367 //if this is not a whole word, continue the search in the string
1368 offset = (options & QTextDocument::FindBackward) ? idx-1 : end+1;
1369 idx = -1;
1370 continue;
1371 }
1372 }
1373 //we have a hit, return the cursor for that.
1374 *cursor = QTextCursorPrivate::fromPosition(d: const_cast<QTextDocumentPrivate *>(QTextDocumentPrivate::get(block)),
1375 pos: block.position() + idx);
1376 cursor->setPosition(pos: cursor->position() + expression.size(), mode: QTextCursor::KeepAnchor);
1377 return true;
1378 }
1379 return false;
1380}
1381
1382/*!
1383 \fn QTextCursor QTextDocument::find(const QString &subString, int position, FindFlags options) const
1384
1385 \overload
1386
1387 Finds the next occurrence of the string, \a subString, in the document.
1388 The search starts at the given \a position, and proceeds forwards
1389 through the document unless specified otherwise in the search options.
1390 The \a options control the type of search performed.
1391
1392 Returns a cursor with the match selected if \a subString
1393 was found; otherwise returns a null cursor.
1394
1395 If the \a position is 0 (the default) the search begins from the beginning
1396 of the document; otherwise it begins at the specified position.
1397*/
1398QTextCursor QTextDocument::find(const QString &subString, int from, FindFlags options) const
1399{
1400 Q_D(const QTextDocument);
1401
1402 if (subString.isEmpty())
1403 return QTextCursor();
1404
1405 int pos = from;
1406 //the cursor is positioned between characters, so for a backward search
1407 //do not include the character given in the position.
1408 if (options & FindBackward) {
1409 --pos ;
1410 if (pos < 0)
1411 return QTextCursor();
1412 }
1413
1414 QTextCursor cursor;
1415 QTextBlock block = d->blocksFind(pos);
1416 int blockOffset = pos - block.position();
1417
1418 if (!(options & FindBackward)) {
1419 while (block.isValid()) {
1420 if (findInBlock(block, expression: subString, offset: blockOffset, options, cursor: &cursor))
1421 return cursor;
1422 block = block.next();
1423 blockOffset = 0;
1424 }
1425 } else {
1426 if (blockOffset == block.length() - 1)
1427 --blockOffset; // make sure to skip end-of-paragraph character
1428 while (block.isValid()) {
1429 if (findInBlock(block, expression: subString, offset: blockOffset, options, cursor: &cursor))
1430 return cursor;
1431 block = block.previous();
1432 blockOffset = block.length() - 2;
1433 }
1434 }
1435
1436 return QTextCursor();
1437}
1438
1439/*!
1440 Finds the next occurrence of the string, \a subString, in the document.
1441 The search starts at the position of the given \a cursor, and proceeds
1442 forwards through the document unless specified otherwise in the search
1443 options. The \a options control the type of search performed.
1444
1445 Returns a cursor with the match selected if \a subString was found; otherwise
1446 returns a null cursor.
1447
1448 If the given \a cursor has a selection, the search begins after the
1449 selection; otherwise it begins at the cursor's position.
1450
1451 By default the search is case insensitive, and can match text anywhere in the
1452 document.
1453*/
1454QTextCursor QTextDocument::find(const QString &subString, const QTextCursor &cursor, FindFlags options) const
1455{
1456 int pos = 0;
1457 if (!cursor.isNull()) {
1458 if (options & QTextDocument::FindBackward)
1459 pos = cursor.selectionStart();
1460 else
1461 pos = cursor.selectionEnd();
1462 }
1463
1464 return find(subString, from: pos, options);
1465}
1466
1467#if QT_CONFIG(regularexpression)
1468static bool findInBlock(const QTextBlock &block, const QRegularExpression &expr, int offset,
1469 QTextDocument::FindFlags options, QTextCursor *cursor)
1470{
1471 QString text = block.text();
1472 text.replace(before: QChar::Nbsp, after: u' ');
1473 QRegularExpressionMatch match;
1474 int idx = -1;
1475
1476 while (offset >= 0 && offset <= text.size()) {
1477 idx = (options & QTextDocument::FindBackward) ?
1478 text.lastIndexOf(re: expr, from: offset, rmatch: &match) : text.indexOf(re: expr, from: offset, rmatch: &match);
1479 if (idx == -1)
1480 return false;
1481
1482 if (options & QTextDocument::FindWholeWords) {
1483 const int start = idx;
1484 const int end = start + match.capturedLength();
1485 if ((start != 0 && text.at(i: start - 1).isLetterOrNumber())
1486 || (end != text.size() && text.at(i: end).isLetterOrNumber())) {
1487 //if this is not a whole word, continue the search in the string
1488 offset = (options & QTextDocument::FindBackward) ? idx-1 : end+1;
1489 idx = -1;
1490 continue;
1491 }
1492 }
1493 //we have a hit, return the cursor for that.
1494 *cursor = QTextCursorPrivate::fromPosition(d: const_cast<QTextDocumentPrivate *>(QTextDocumentPrivate::get(block)),
1495 pos: block.position() + idx);
1496 cursor->setPosition(pos: cursor->position() + match.capturedLength(), mode: QTextCursor::KeepAnchor);
1497 return true;
1498 }
1499 return false;
1500}
1501
1502/*!
1503 \since 5.5
1504
1505 Finds the next occurrence that matches the given regular expression,
1506 \a expr, within the same paragraph in the document.
1507
1508 The search starts at the given \a from position, and proceeds forwards
1509 through the document unless specified otherwise in the search options.
1510 The \a options control the type of search performed.
1511
1512 Returns a cursor with the match selected if a match was found; otherwise
1513 returns a null cursor.
1514
1515 If the \a from position is 0 (the default) the search begins from the beginning
1516 of the document; otherwise it begins at the specified position.
1517
1518 \warning For historical reasons, the case sensitivity option set on
1519 \a expr is ignored. Instead, the \a options are used to determine
1520 if the search is case sensitive or not.
1521*/
1522QTextCursor QTextDocument::find(const QRegularExpression &expr, int from, FindFlags options) const
1523{
1524 Q_D(const QTextDocument);
1525
1526 if (!expr.isValid())
1527 return QTextCursor();
1528
1529 int pos = from;
1530 //the cursor is positioned between characters, so for a backward search
1531 //do not include the character given in the position.
1532 if (options & FindBackward) {
1533 --pos ;
1534 if (pos < 0)
1535 return QTextCursor();
1536 }
1537
1538 QTextCursor cursor;
1539 QTextBlock block = d->blocksFind(pos);
1540 int blockOffset = pos - block.position();
1541
1542 QRegularExpression expression(expr);
1543 if (!(options & QTextDocument::FindCaseSensitively))
1544 expression.setPatternOptions(expr.patternOptions() | QRegularExpression::CaseInsensitiveOption);
1545 else
1546 expression.setPatternOptions(expr.patternOptions() & ~QRegularExpression::CaseInsensitiveOption);
1547
1548 if (!(options & FindBackward)) {
1549 while (block.isValid()) {
1550 if (findInBlock(block, expr: expression, offset: blockOffset, options, cursor: &cursor))
1551 return cursor;
1552 block = block.next();
1553 blockOffset = 0;
1554 }
1555 } else {
1556 while (block.isValid()) {
1557 if (findInBlock(block, expr: expression, offset: blockOffset, options, cursor: &cursor))
1558 return cursor;
1559 block = block.previous();
1560 blockOffset = block.length() - 1;
1561 }
1562 }
1563
1564 return QTextCursor();
1565}
1566
1567/*!
1568 \since 5.5
1569
1570 Finds the next occurrence that matches the given regular expression,
1571 \a expr, within the same paragraph in the document.
1572
1573 The search starts at the position of the given \a cursor, and proceeds
1574 forwards through the document unless specified otherwise in the search
1575 options. The \a options control the type of search performed.
1576
1577 Returns a cursor with the match selected if a match was found; otherwise
1578 returns a null cursor.
1579
1580 If the given \a cursor has a selection, the search begins after the
1581 selection; otherwise it begins at the cursor's position.
1582
1583 By default the search is case insensitive, and can match text anywhere in the
1584 document.
1585*/
1586QTextCursor QTextDocument::find(const QRegularExpression &expr, const QTextCursor &cursor, FindFlags options) const
1587{
1588 int pos = 0;
1589 if (!cursor.isNull()) {
1590 if (options & QTextDocument::FindBackward)
1591 pos = cursor.selectionStart();
1592 else
1593 pos = cursor.selectionEnd();
1594 }
1595 return find(expr, from: pos, options);
1596}
1597#endif // QT_CONFIG(regularexpression)
1598
1599/*!
1600 \fn QTextObject *QTextDocument::createObject(const QTextFormat &format)
1601
1602 Creates and returns a new document object (a QTextObject), based
1603 on the given \a format.
1604
1605 QTextObjects will always get created through this method, so you
1606 must reimplement it if you use custom text objects inside your document.
1607*/
1608QTextObject *QTextDocument::createObject(const QTextFormat &f)
1609{
1610 QTextObject *obj = nullptr;
1611 if (f.isListFormat())
1612 obj = new QTextList(this);
1613 else if (f.isTableFormat())
1614 obj = new QTextTable(this);
1615 else if (f.isFrameFormat())
1616 obj = new QTextFrame(this);
1617
1618 return obj;
1619}
1620
1621/*!
1622 \internal
1623
1624 Returns the frame that contains the text cursor position \a pos.
1625*/
1626QTextFrame *QTextDocument::frameAt(int pos) const
1627{
1628 Q_D(const QTextDocument);
1629 return d->frameAt(pos);
1630}
1631
1632/*!
1633 Returns the document's root frame.
1634*/
1635QTextFrame *QTextDocument::rootFrame() const
1636{
1637 Q_D(const QTextDocument);
1638 return d->rootFrame();
1639}
1640
1641/*!
1642 Returns the text object associated with the given \a objectIndex.
1643*/
1644QTextObject *QTextDocument::object(int objectIndex) const
1645{
1646 Q_D(const QTextDocument);
1647 return d->objectForIndex(objectIndex);
1648}
1649
1650/*!
1651 Returns the text object associated with the format \a f.
1652*/
1653QTextObject *QTextDocument::objectForFormat(const QTextFormat &f) const
1654{
1655 Q_D(const QTextDocument);
1656 return d->objectForFormat(f);
1657}
1658
1659
1660/*!
1661 Returns the text block that contains the \a{pos}-th character.
1662*/
1663QTextBlock QTextDocument::findBlock(int pos) const
1664{
1665 Q_D(const QTextDocument);
1666 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), d->blockMap().findNode(k: pos));
1667}
1668
1669/*!
1670 \since 4.4
1671 Returns the text block with the specified \a blockNumber.
1672
1673 \sa QTextBlock::blockNumber()
1674*/
1675QTextBlock QTextDocument::findBlockByNumber(int blockNumber) const
1676{
1677 Q_D(const QTextDocument);
1678 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), d->blockMap().findNode(k: blockNumber, field: 1));
1679}
1680
1681/*!
1682 \since 4.5
1683 Returns the text block that contains the specified \a lineNumber.
1684
1685 \sa QTextBlock::firstLineNumber()
1686*/
1687QTextBlock QTextDocument::findBlockByLineNumber(int lineNumber) const
1688{
1689 Q_D(const QTextDocument);
1690 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), d->blockMap().findNode(k: lineNumber, field: 2));
1691}
1692
1693/*!
1694 Returns the document's first text block.
1695
1696 \sa firstBlock()
1697*/
1698QTextBlock QTextDocument::begin() const
1699{
1700 Q_D(const QTextDocument);
1701 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), d->blockMap().begin().n);
1702}
1703
1704/*!
1705 This function returns a block to test for the end of the document
1706 while iterating over it.
1707
1708 \snippet textdocument-end/textdocumentendsnippet.cpp 0
1709
1710 The block returned is invalid and represents the block after the
1711 last block in the document. You can use lastBlock() to retrieve the
1712 last valid block of the document.
1713
1714 \sa lastBlock()
1715*/
1716QTextBlock QTextDocument::end() const
1717{
1718 Q_D(const QTextDocument);
1719 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), 0);
1720}
1721
1722/*!
1723 \since 4.4
1724 Returns the document's first text block.
1725*/
1726QTextBlock QTextDocument::firstBlock() const
1727{
1728 Q_D(const QTextDocument);
1729 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), d->blockMap().begin().n);
1730}
1731
1732/*!
1733 \since 4.4
1734 Returns the document's last (valid) text block.
1735*/
1736QTextBlock QTextDocument::lastBlock() const
1737{
1738 Q_D(const QTextDocument);
1739 return QTextBlock(const_cast<QTextDocumentPrivate *>(d), d->blockMap().last().n);
1740}
1741
1742/*!
1743 \property QTextDocument::pageSize
1744 \brief the page size that should be used for laying out the document
1745
1746 The units are determined by the underlying paint device. The size is
1747 measured in logical pixels when painting to the screen, and in points
1748 (1/72 inch) when painting to a printer.
1749
1750 By default, for a newly-created, empty document, this property contains
1751 an undefined size.
1752
1753 \sa modificationChanged()
1754*/
1755
1756void QTextDocument::setPageSize(const QSizeF &size)
1757{
1758 Q_D(QTextDocument);
1759 d->pageSize = size;
1760 if (d->lout)
1761 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
1762}
1763
1764QSizeF QTextDocument::pageSize() const
1765{
1766 Q_D(const QTextDocument);
1767 return d->pageSize;
1768}
1769
1770/*!
1771 returns the number of pages in this document.
1772*/
1773int QTextDocument::pageCount() const
1774{
1775 return documentLayout()->pageCount();
1776}
1777
1778/*!
1779 Sets the default \a font to use in the document layout.
1780*/
1781void QTextDocument::setDefaultFont(const QFont &font)
1782{
1783 Q_D(QTextDocument);
1784 d->setDefaultFont(font);
1785 if (d->lout)
1786 d->lout->documentChanged(from: 0, charsRemoved: 0, charsAdded: d->length());
1787}
1788
1789/*!
1790 Returns the default font to be used in the document layout.
1791*/
1792QFont QTextDocument::defaultFont() const
1793{
1794 Q_D(const QTextDocument);
1795 return d->defaultFont();
1796}
1797
1798/*!
1799 \fn void QTextDocument::setSuperScriptBaseline(qreal baseline)
1800 \since 6.0
1801
1802 Sets the default superscript's base line as a % of font height to use in the document
1803 layout to \a baseline. The default value is 50% (1/2 of height).
1804
1805 \sa superScriptBaseline(), setSubScriptBaseline(), subScriptBaseline(), setBaselineOffset(), baselineOffset()
1806*/
1807void QTextDocument::setSuperScriptBaseline(qreal baseline)
1808{
1809 Q_D(QTextDocument);
1810 d->formats.setSuperScriptBaseline(baseline);
1811}
1812
1813/*!
1814 \fn qreal QTextDocument::superScriptBaseline() const
1815 \since 6.0
1816
1817 Returns the superscript's base line as a % of font height used in the document layout.
1818
1819 \sa setSuperScriptBaseline(), setSubScriptBaseline(), subScriptBaseline(), setBaselineOffset(), baselineOffset()
1820*/
1821qreal QTextDocument::superScriptBaseline() const
1822{
1823 Q_D(const QTextDocument);
1824 return d->formats.defaultTextFormat().superScriptBaseline();
1825}
1826
1827/*!
1828 \fn void QTextDocument::setSubScriptBaseline(qreal baseline)
1829 \since 6.0
1830
1831 Sets the default subscript's base line as a % of font height to use in the document layout
1832 to \a baseline. The default value is 16.67% (1/6 of height).
1833
1834 \sa subScriptBaseline(), setSuperScriptBaseline(), superScriptBaseline(), setBaselineOffset(), baselineOffset()
1835*/
1836void QTextDocument::setSubScriptBaseline(qreal baseline)
1837{
1838 Q_D(QTextDocument);
1839 d->formats.setSubScriptBaseline(baseline);
1840}
1841
1842/*!
1843 \fn qreal QTextDocument::subScriptBaseline() const
1844 \since 6.0
1845
1846 Returns the superscript's base line as a % of font height used in the document layout.
1847
1848 \sa setSubScriptBaseline(), setSuperScriptBaseline(), superScriptBaseline(), setBaselineOffset(), baselineOffset()
1849*/
1850qreal QTextDocument::subScriptBaseline() const
1851{
1852 Q_D(const QTextDocument);
1853 return d->formats.defaultTextFormat().subScriptBaseline();
1854}
1855
1856/*!
1857 \fn void QTextDocument::setBaselineOffset(qreal baseline)
1858 \since 6.0
1859
1860 Sets the base line as a% of font height to use in the document layout to \a baseline.
1861 The default value is 0.
1862 A positive value moves up the text, by the corresponding %; a negative value moves it down.
1863
1864 \sa baselineOffset(), setSubScriptBaseline(), subScriptBaseline(), setSuperScriptBaseline(), superScriptBaseline()
1865*/
1866void QTextDocument::setBaselineOffset(qreal baseline)
1867{
1868 Q_D(QTextDocument);
1869 d->formats.setBaselineOffset(baseline);
1870}
1871
1872/*!
1873 \fn qreal QTextDocument::baselineOffset() const
1874 \since 6.0
1875
1876 Returns the baseline offset in % used in the document layout.
1877
1878 \sa setBaselineOffset(), setSubScriptBaseline(), subScriptBaseline(), setSuperScriptBaseline(),
1879 superScriptBaseline()
1880*/
1881qreal QTextDocument::baselineOffset() const
1882{
1883 Q_D(const QTextDocument);
1884 return d->formats.defaultTextFormat().baselineOffset();
1885}
1886
1887/*!
1888 \fn void QTextDocument::modificationChanged(bool changed)
1889
1890 This signal is emitted whenever the content of the document
1891 changes in a way that affects the modification state. If \a
1892 changed is true, the document has been modified; otherwise it is
1893 false.
1894
1895 For example, calling setModified(false) on a document and then
1896 inserting text causes the signal to get emitted. If you undo that
1897 operation, causing the document to return to its original
1898 unmodified state, the signal will get emitted again.
1899*/
1900
1901/*!
1902 \property QTextDocument::modified
1903 \brief whether the document has been modified by the user
1904
1905 By default, this property is \c false.
1906
1907 \sa modificationChanged()
1908*/
1909
1910bool QTextDocument::isModified() const
1911{
1912 Q_D(const QTextDocument);
1913 return d->isModified();
1914}
1915
1916void QTextDocument::setModified(bool m)
1917{
1918 Q_D(QTextDocument);
1919 d->setModified(m);
1920}
1921
1922#ifndef QT_NO_PRINTER
1923static void printPage(int index, QPainter *painter, const QTextDocument *doc, const QRectF &body, const QPointF &pageNumberPos)
1924{
1925 painter->save();
1926 painter->translate(dx: body.left(), dy: body.top() - (index - 1) * body.height());
1927 QRectF view(0, (index - 1) * body.height(), body.width(), body.height());
1928
1929 QAbstractTextDocumentLayout *layout = doc->documentLayout();
1930 QAbstractTextDocumentLayout::PaintContext ctx;
1931
1932 painter->setClipRect(view);
1933 ctx.clip = view;
1934
1935 // don't use the system palette text as default text color, on HP/UX
1936 // for example that's white, and white text on white paper doesn't
1937 // look that nice
1938 ctx.palette.setColor(acr: QPalette::Text, acolor: Qt::black);
1939
1940 layout->draw(painter, context: ctx);
1941
1942 if (!pageNumberPos.isNull()) {
1943 painter->setClipping(false);
1944 painter->setFont(QFont(doc->defaultFont()));
1945 const QString pageString = QString::number(index);
1946
1947 painter->drawText(x: qRound(d: pageNumberPos.x() - painter->fontMetrics().horizontalAdvance(pageString)),
1948 y: qRound(d: pageNumberPos.y() + view.top()),
1949 s: pageString);
1950 }
1951
1952 painter->restore();
1953}
1954
1955/*!
1956 Prints the document to the given \a printer. The QPagedPaintDevice must be
1957 set up before being used with this function.
1958
1959 This is only a convenience method to print the whole document to the printer.
1960
1961 If the document is already paginated through a specified height in the pageSize()
1962 property it is printed as-is.
1963
1964 If the document is not paginated, like for example a document used in a QTextEdit,
1965 then a temporary copy of the document is created and the copy is broken into
1966 multiple pages according to the size of the paint device's paperRect(). By default
1967 a 2 cm margin is set around the document contents. In addition the current page
1968 number is printed at the bottom of each page.
1969
1970 \sa QTextEdit::print()
1971*/
1972
1973void QTextDocument::print(QPagedPaintDevice *printer) const
1974{
1975 Q_D(const QTextDocument);
1976
1977 if (!printer)
1978 return;
1979
1980 bool documentPaginated = d->pageSize.isValid() && !d->pageSize.isNull()
1981 && d->pageSize.height() != INT_MAX;
1982
1983 // ### set page size to paginated size?
1984 QMarginsF m = printer->pageLayout().margins(units: QPageLayout::Millimeter);
1985 if (!documentPaginated && m.left() == 0. && m.right() == 0. && m.top() == 0. && m.bottom() == 0.) {
1986 m.setLeft(2);
1987 m.setRight(2);
1988 m.setTop(2);
1989 m.setBottom(2);
1990 printer->setPageMargins(margins: m, units: QPageLayout::Millimeter);
1991 }
1992 // ### use the margins correctly
1993
1994 QPainter p(printer);
1995
1996 // Check that there is a valid device to print to.
1997 if (!p.isActive())
1998 return;
1999
2000 const QTextDocument *doc = this;
2001 QScopedPointer<QTextDocument> clonedDoc;
2002 (void)doc->documentLayout(); // make sure that there is a layout
2003
2004 QRectF body = QRectF(QPointF(0, 0), d->pageSize);
2005 QPointF pageNumberPos;
2006
2007 qreal sourceDpiX = qt_defaultDpiX();
2008 qreal sourceDpiY = qt_defaultDpiY();
2009 const qreal dpiScaleX = qreal(printer->logicalDpiX()) / sourceDpiX;
2010 const qreal dpiScaleY = qreal(printer->logicalDpiY()) / sourceDpiY;
2011
2012 if (documentPaginated) {
2013
2014 QPaintDevice *dev = doc->documentLayout()->paintDevice();
2015 if (dev) {
2016 sourceDpiX = dev->logicalDpiX();
2017 sourceDpiY = dev->logicalDpiY();
2018 }
2019
2020 // scale to dpi
2021 p.scale(sx: dpiScaleX, sy: dpiScaleY);
2022
2023 QSizeF scaledPageSize = d->pageSize;
2024 scaledPageSize.rwidth() *= dpiScaleX;
2025 scaledPageSize.rheight() *= dpiScaleY;
2026
2027 const QSizeF printerPageSize(printer->width(), printer->height());
2028
2029 // scale to page
2030 p.scale(sx: printerPageSize.width() / scaledPageSize.width(),
2031 sy: printerPageSize.height() / scaledPageSize.height());
2032 } else {
2033 doc = clone(parent: const_cast<QTextDocument *>(this));
2034 clonedDoc.reset(other: const_cast<QTextDocument *>(doc));
2035
2036 for (QTextBlock srcBlock = firstBlock(), dstBlock = clonedDoc->firstBlock();
2037 srcBlock.isValid() && dstBlock.isValid();
2038 srcBlock = srcBlock.next(), dstBlock = dstBlock.next()) {
2039 dstBlock.layout()->setFormats(srcBlock.layout()->formats());
2040 }
2041
2042 QAbstractTextDocumentLayout *layout = doc->documentLayout();
2043 layout->setPaintDevice(p.device());
2044
2045 // copy the custom object handlers
2046 layout->d_func()->handlers = documentLayout()->d_func()->handlers;
2047
2048 // 2 cm margins, scaled to device in QTextDocumentLayoutPrivate::layoutFrame
2049 const int horizontalMargin = int((2/2.54)*sourceDpiX);
2050 const int verticalMargin = int((2/2.54)*sourceDpiY);
2051 QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
2052 fmt.setLeftMargin(horizontalMargin);
2053 fmt.setRightMargin(horizontalMargin);
2054 fmt.setTopMargin(verticalMargin);
2055 fmt.setBottomMargin(verticalMargin);
2056 doc->rootFrame()->setFrameFormat(fmt);
2057
2058 // pageNumberPos must be in device coordinates, so scale to device here
2059 const int dpiy = p.device()->logicalDpiY();
2060 body = QRectF(0, 0, printer->width(), printer->height());
2061 pageNumberPos = QPointF(body.width() - horizontalMargin * dpiScaleX,
2062 body.height() - verticalMargin * dpiScaleY
2063 + QFontMetrics(doc->defaultFont(), p.device()).ascent()
2064 + 5 * dpiy / 72.0);
2065 clonedDoc->setPageSize(body.size());
2066 }
2067
2068 const QPageRanges pageRanges = printer->pageRanges();
2069 int fromPage = pageRanges.firstPage();
2070 int toPage = pageRanges.lastPage();
2071
2072 if (fromPage == 0 && toPage == 0) {
2073 fromPage = 1;
2074 toPage = doc->pageCount();
2075 }
2076 // paranoia check
2077 fromPage = qMax(a: 1, b: fromPage);
2078 toPage = qMin(a: doc->pageCount(), b: toPage);
2079
2080 if (toPage < fromPage) {
2081 // if the user entered a page range outside the actual number
2082 // of printable pages, just return
2083 return;
2084 }
2085
2086// bool ascending = true;
2087// if (printer->pageOrder() == QPrinter::LastPageFirst) {
2088// int tmp = fromPage;
2089// fromPage = toPage;
2090// toPage = tmp;
2091// ascending = false;
2092// }
2093
2094 int page = fromPage;
2095 while (true) {
2096 if (pageRanges.isEmpty() || pageRanges.contains(pageNumber: page))
2097 printPage(index: page, painter: &p, doc, body, pageNumberPos);
2098
2099 if (page == toPage)
2100 break;
2101 ++page;
2102 if (!printer->newPage())
2103 return;
2104 }
2105}
2106#endif
2107
2108/*!
2109 \enum QTextDocument::ResourceType
2110
2111 This enum describes the types of resources that can be loaded by
2112 QTextDocument's loadResource() function or by QTextBrowser::setSource().
2113
2114 \value UnknownResource No resource is loaded, or the resource type is not known.
2115 \value HtmlResource The resource contains HTML.
2116 \value ImageResource The resource contains image data.
2117 Currently supported data types are QMetaType::QPixmap and
2118 QMetaType::QImage. If the corresponding variant is of type
2119 QMetaType::QByteArray then Qt attempts to load the image using
2120 QImage::loadFromData. QMetaType::QIcon is currently not supported.
2121 The icon needs to be converted to one of the supported types first,
2122 for example using QIcon::pixmap.
2123 \value StyleSheetResource The resource contains CSS.
2124 \value MarkdownResource The resource contains Markdown.
2125 \value UserResource The first available value for user defined
2126 resource types.
2127
2128 \sa loadResource(), QTextBrowser::sourceType()
2129*/
2130
2131/*!
2132 Returns data of the specified \a type from the resource with the
2133 given \a name.
2134
2135 This function is called by the rich text engine to request data that isn't
2136 directly stored by QTextDocument, but still associated with it. For example,
2137 images are referenced indirectly by the name attribute of a QTextImageFormat
2138 object.
2139
2140 Resources are cached internally in the document. If a resource can
2141 not be found in the cache, loadResource is called to try to load
2142 the resource. loadResource should then use addResource to add the
2143 resource to the cache.
2144
2145 If loadResource does not load the resource, then the resourceProvider and
2146 lastly the defaultResourceProvider will be called, if set. Note that the
2147 result from the provider will not be added automatically to the cache.
2148
2149 \sa QTextDocument::ResourceType, resourceProvider()
2150*/
2151QVariant QTextDocument::resource(int type, const QUrl &name) const
2152{
2153 Q_D(const QTextDocument);
2154 const QUrl url = d->baseUrl.resolved(relative: name);
2155 QVariant r = d->resources.value(key: url);
2156 if (!r.isValid()) {
2157 r = d->cachedResources.value(key: url);
2158 if (!r.isValid()) {
2159 r = const_cast<QTextDocument *>(this)->loadResource(type, name: url);
2160 if (!r.isValid()) {
2161 if (d->resourceProvider)
2162 r = d->resourceProvider(url);
2163 else if (auto defaultProvider = defaultResourceProvider())
2164 r = defaultProvider(url);
2165 }
2166 }
2167 }
2168 return r;
2169}
2170
2171/*!
2172 Adds the resource \a resource to the resource cache, using \a
2173 type and \a name as identifiers. \a type should be a value from
2174 QTextDocument::ResourceType.
2175
2176 For example, you can add an image as a resource in order to reference it
2177 from within the document:
2178
2179 \snippet textdocument-resources/main.cpp Adding a resource
2180
2181 The image can be inserted into the document using the QTextCursor API:
2182
2183 \snippet textdocument-resources/main.cpp Inserting an image with a cursor
2184
2185 Alternatively, you can insert images using the HTML \c img tag:
2186
2187 \snippet textdocument-resources/main.cpp Inserting an image using HTML
2188*/
2189void QTextDocument::addResource(int type, const QUrl &name, const QVariant &resource)
2190{
2191 Q_UNUSED(type);
2192 Q_D(QTextDocument);
2193 d->resources.insert(key: name, value: resource);
2194}
2195
2196/*!
2197 \since 6.1
2198
2199 Returns the resource provider for this text document.
2200
2201 \sa setResourceProvider(), defaultResourceProvider(), loadResource()
2202*/
2203QTextDocument::ResourceProvider QTextDocument::resourceProvider() const
2204{
2205 Q_D(const QTextDocument);
2206 return d->resourceProvider;
2207}
2208
2209/*!
2210 \since 6.1
2211 \typealias QTextDocument::ResourceProvider
2212
2213 Type alias for std::function\<QVariant(const QUrl&)\>.
2214*/
2215
2216/*!
2217 \since 6.1
2218
2219 Sets the provider of resources for the text document to \a provider.
2220
2221 \sa resourceProvider(), loadResource()
2222*/
2223void QTextDocument::setResourceProvider(const ResourceProvider &provider)
2224{
2225 Q_D(QTextDocument);
2226 d->resourceProvider = provider;
2227}
2228
2229/*!
2230 \since 6.1
2231
2232 Sets the default resource provider to \a provider.
2233
2234 The default provider will be used by all QTextDocuments that don't have an
2235 explicit provider set.
2236
2237 \sa setResourceProvider(), loadResource()
2238*/
2239void QTextDocument::setDefaultResourceProvider(const ResourceProvider &provider)
2240{
2241 qt_defaultResourceProvider = provider;
2242}
2243
2244/*!
2245 \since 6.1
2246
2247 Returns the default resource provider.
2248
2249 \sa resourceProvider(), loadResource()
2250*/
2251QTextDocument::ResourceProvider QTextDocument::defaultResourceProvider()
2252{
2253 return qt_defaultResourceProvider;
2254}
2255
2256/*!
2257 Loads data of the specified \a type from the resource with the
2258 given \a name.
2259
2260 This function is called by the rich text engine to request data that isn't
2261 directly stored by QTextDocument, but still associated with it. For example,
2262 images are referenced indirectly by the name attribute of a QTextImageFormat
2263 object.
2264
2265 When called by Qt, \a type is one of the values of
2266 QTextDocument::ResourceType.
2267
2268 If the QTextDocument is a child object of a QObject that has an invokable
2269 loadResource method such as QTextEdit, QTextBrowser
2270 or a QTextDocument itself then the default implementation tries
2271 to retrieve the data from the parent.
2272
2273 \sa QTextDocument::ResourceProvider
2274*/
2275QVariant QTextDocument::loadResource(int type, const QUrl &name)
2276{
2277 Q_D(QTextDocument);
2278 QVariant r;
2279
2280 QObject *p = parent();
2281 if (p) {
2282 const QMetaObject *me = p->metaObject();
2283 int index = me->indexOfMethod(method: "loadResource(int,QUrl)");
2284 if (index >= 0) {
2285 QMetaMethod loader = me->method(index);
2286 // don't invoke() via a queued connection: this function needs to return a value
2287 loader.invoke(obj: p, c: Qt::DirectConnection, Q_RETURN_ARG(QVariant, r), Q_ARG(int, type), Q_ARG(QUrl, name));
2288 }
2289 }
2290
2291 // handle data: URLs
2292 if (r.isNull() && name.scheme().compare(other: "data"_L1, cs: Qt::CaseInsensitive) == 0) {
2293 QString mimetype;
2294 QByteArray payload;
2295 if (qDecodeDataUrl(url: name, mimeType&: mimetype, payload))
2296 r = payload;
2297 }
2298
2299 // if resource was not loaded try to load it here
2300 if (!qobject_cast<QTextDocument *>(object: p) && r.isNull()) {
2301 QUrl resourceUrl = name;
2302
2303 if (name.isRelative()) {
2304 QUrl currentURL = d->url;
2305 // For the second case QUrl can merge "#someanchor" with "foo.html"
2306 // correctly to "foo.html#someanchor"
2307 if (!(currentURL.isRelative()
2308 || (currentURL.scheme() == "file"_L1
2309 && !QFileInfo(currentURL.toLocalFile()).isAbsolute()))
2310 || (name.hasFragment() && name.path().isEmpty())) {
2311 resourceUrl = currentURL.resolved(relative: name);
2312 } else {
2313 // this is our last resort when current url and new url are both relative
2314 // we try to resolve against the current working directory in the local
2315 // file system.
2316 QFileInfo fi(currentURL.toLocalFile());
2317 if (fi.exists()) {
2318 resourceUrl =
2319 QUrl::fromLocalFile(localfile: fi.absolutePath() + QDir::separator()).resolved(relative: name);
2320 } else if (currentURL.isEmpty()) {
2321 resourceUrl.setScheme("file"_L1);
2322 }
2323 }
2324 }
2325
2326 QString s = resourceUrl.toLocalFile();
2327 QFile f(s);
2328 if (!s.isEmpty() && f.open(flags: QFile::ReadOnly)) {
2329 r = f.readAll();
2330 f.close();
2331 }
2332 }
2333
2334 if (!r.isNull()) {
2335 if (type == ImageResource && r.userType() == QMetaType::QByteArray) {
2336 if (qApp->thread() != QThread::currentThread()) {
2337 // must use images in non-GUI threads
2338 QImage image;
2339 image.loadFromData(data: r.toByteArray());
2340 if (!image.isNull())
2341 r = image;
2342 } else {
2343 QPixmap pm;
2344 pm.loadFromData(buf: r.toByteArray());
2345 if (!pm.isNull())
2346 r = pm;
2347 }
2348 }
2349 d->cachedResources.insert(key: name, value: r);
2350 }
2351 return r;
2352}
2353
2354static QTextFormat formatDifference(const QTextFormat &from, const QTextFormat &to)
2355{
2356 QTextFormat diff = to;
2357
2358 const QMap<int, QVariant> props = to.properties();
2359 for (QMap<int, QVariant>::ConstIterator it = props.begin(), end = props.end();
2360 it != end; ++it)
2361 if (it.value() == from.property(propertyId: it.key()))
2362 diff.clearProperty(propertyId: it.key());
2363
2364 return diff;
2365}
2366
2367static QString colorValue(QColor color)
2368{
2369 QString result;
2370
2371 if (color.alpha() == 255) {
2372 result = color.name();
2373 } else if (color.alpha()) {
2374 QString alphaValue = QString::number(color.alphaF(), format: 'f', precision: 6);
2375 while (alphaValue.size() > 1 && alphaValue.at(i: alphaValue.size() - 1) == u'0')
2376 alphaValue.chop(n: 1);
2377 if (alphaValue.at(i: alphaValue.size() - 1) == u'.')
2378 alphaValue.chop(n: 1);
2379 result = QString::fromLatin1(ba: "rgba(%1,%2,%3,%4)").arg(a: color.red())
2380 .arg(a: color.green())
2381 .arg(a: color.blue())
2382 .arg(a: alphaValue);
2383 } else {
2384 result = "transparent"_L1;
2385 }
2386
2387 return result;
2388}
2389
2390QTextHtmlExporter::QTextHtmlExporter(const QTextDocument *_doc)
2391 : doc(_doc), fragmentMarkers(false)
2392{
2393 const QFont defaultFont = doc->defaultFont();
2394 defaultCharFormat.setFont(font: defaultFont);
2395}
2396
2397static QStringList resolvedFontFamilies(const QTextCharFormat &format)
2398{
2399 return format.fontFamilies().toStringList();
2400}
2401
2402/*!
2403 Returns the document in HTML format. The conversion may not be
2404 perfect, especially for complex documents, due to the limitations
2405 of HTML.
2406*/
2407QString QTextHtmlExporter::toHtml(ExportMode mode)
2408{
2409 html = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" "
2410 "\"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
2411 "<html><head><meta name=\"qrichtext\" content=\"1\" />"_L1;
2412 html.reserve(asize: QTextDocumentPrivate::get(document: doc)->length());
2413
2414 fragmentMarkers = (mode == ExportFragment);
2415
2416 html += "<meta charset=\"utf-8\" />"_L1;
2417
2418 QString title = doc->metaInformation(info: QTextDocument::DocumentTitle);
2419 if (!title.isEmpty()) {
2420 html += "<title>"_L1;
2421 html += title;
2422 html += "</title>"_L1;
2423 }
2424 html += "<style type=\"text/css\">\n"_L1;
2425 html += "p, li { white-space: pre-wrap; }\n"_L1;
2426 html += "hr { height: 1px; border-width: 0; }\n"_L1;
2427 html += "li.unchecked::marker { content: \"\\2610\"; }\n"_L1;
2428 html += "li.checked::marker { content: \"\\2612\"; }\n"_L1;
2429 html += "</style>"_L1;
2430 html += "</head><body"_L1;
2431
2432 if (mode == ExportEntireDocument) {
2433 html += " style=\""_L1;
2434
2435 emitFontFamily(families: resolvedFontFamilies(format: defaultCharFormat));
2436
2437 if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontPointSize)) {
2438 html += " font-size:"_L1;
2439 html += QString::number(defaultCharFormat.fontPointSize());
2440 html += "pt;"_L1;
2441 } else if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontPixelSize)) {
2442 html += " font-size:"_L1;
2443 html += QString::number(defaultCharFormat.intProperty(propertyId: QTextFormat::FontPixelSize));
2444 html += "px;"_L1;
2445 }
2446
2447 html += " font-weight:"_L1;
2448 html += QString::number(defaultCharFormat.fontWeight());
2449 html += u';';
2450
2451 html += " font-style:"_L1;
2452 html += (defaultCharFormat.fontItalic() ? "italic"_L1 : "normal"_L1);
2453 html += u';';
2454
2455 const bool percentSpacing = (defaultCharFormat.fontLetterSpacingType() == QFont::PercentageSpacing);
2456 if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontLetterSpacing) &&
2457 (!percentSpacing || defaultCharFormat.fontLetterSpacing() != 0.0)) {
2458 html += " letter-spacing:"_L1;
2459 qreal value = defaultCharFormat.fontLetterSpacing();
2460 if (percentSpacing) // Map to em (100% == 0em)
2461 value = (value / 100) - 1;
2462 html += QString::number(value);
2463 html += percentSpacing ? "em;"_L1 : "px;"_L1;
2464 }
2465
2466 if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontWordSpacing) &&
2467 defaultCharFormat.fontWordSpacing() != 0.0) {
2468 html += " word-spacing:"_L1;
2469 html += QString::number(defaultCharFormat.fontWordSpacing());
2470 html += "px;"_L1;
2471 }
2472
2473 QString decorationTag(" text-decoration:"_L1);
2474 bool atLeastOneDecorationSet = false;
2475 if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontUnderline) || defaultCharFormat.hasProperty(propertyId: QTextFormat::TextUnderlineStyle)) {
2476 if (defaultCharFormat.fontUnderline()) {
2477 decorationTag += " underline"_L1;
2478 atLeastOneDecorationSet = true;
2479 }
2480 }
2481 if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontOverline)) {
2482 if (defaultCharFormat.fontOverline()) {
2483 decorationTag += " overline"_L1;
2484 atLeastOneDecorationSet = true;
2485 }
2486 }
2487 if (defaultCharFormat.hasProperty(propertyId: QTextFormat::FontStrikeOut)) {
2488 if (defaultCharFormat.fontStrikeOut()) {
2489 decorationTag += " line-through"_L1;
2490 atLeastOneDecorationSet = true;
2491 }
2492 }
2493 if (atLeastOneDecorationSet)
2494 html += decorationTag + u';';
2495
2496 html += u'\"';
2497
2498 const QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
2499 emitBackgroundAttribute(format: fmt);
2500
2501 } else {
2502 defaultCharFormat = QTextCharFormat();
2503 }
2504 html += u'>';
2505
2506 QTextFrameFormat rootFmt = doc->rootFrame()->frameFormat();
2507 rootFmt.clearProperty(propertyId: QTextFormat::BackgroundBrush);
2508
2509 QTextFrameFormat defaultFmt;
2510 defaultFmt.setMargin(doc->documentMargin());
2511
2512 if (rootFmt == defaultFmt)
2513 emitFrame(frameIt: doc->rootFrame()->begin());
2514 else
2515 emitTextFrame(frame: doc->rootFrame());
2516
2517 html += "</body></html>"_L1;
2518 return html;
2519}
2520
2521void QTextHtmlExporter::emitAttribute(const char *attribute, const QString &value)
2522{
2523 html += u' ';
2524 html += QLatin1StringView(attribute);
2525 html += "=\""_L1;
2526 html += value.toHtmlEscaped();
2527 html += u'"';
2528}
2529
2530bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format)
2531{
2532 bool attributesEmitted = false;
2533
2534 {
2535 const QStringList families = resolvedFontFamilies(format);
2536 if (!families.isEmpty() && families != resolvedFontFamilies(format: defaultCharFormat)) {
2537 emitFontFamily(families);
2538 attributesEmitted = true;
2539 }
2540 }
2541
2542 if (format.hasProperty(propertyId: QTextFormat::FontPointSize)
2543 && format.fontPointSize() != defaultCharFormat.fontPointSize()) {
2544 html += " font-size:"_L1;
2545 html += QString::number(format.fontPointSize());
2546 html += "pt;"_L1;
2547 attributesEmitted = true;
2548 } else if (format.hasProperty(propertyId: QTextFormat::FontSizeAdjustment)) {
2549 static const char sizeNameData[] =
2550 "small" "\0"
2551 "medium" "\0"
2552 "xx-large" ;
2553 static const quint8 sizeNameOffsets[] = {
2554 0, // "small"
2555 sizeof("small"), // "medium"
2556 sizeof("small") + sizeof("medium") + 3, // "large" )
2557 sizeof("small") + sizeof("medium") + 1, // "x-large" )> compressed into "xx-large"
2558 sizeof("small") + sizeof("medium"), // "xx-large" )
2559 };
2560 const char *name = nullptr;
2561 const int idx = format.intProperty(propertyId: QTextFormat::FontSizeAdjustment) + 1;
2562 if (idx >= 0 && idx <= 4) {
2563 name = sizeNameData + sizeNameOffsets[idx];
2564 }
2565 if (name) {
2566 html += " font-size:"_L1;
2567 html += QLatin1StringView(name);
2568 html += u';';
2569 attributesEmitted = true;
2570 }
2571 } else if (format.hasProperty(propertyId: QTextFormat::FontPixelSize)
2572 && format.property(propertyId: QTextFormat::FontPixelSize)
2573 != defaultCharFormat.property(propertyId: QTextFormat::FontPixelSize)) {
2574 html += " font-size:"_L1;
2575 html += QString::number(format.intProperty(propertyId: QTextFormat::FontPixelSize));
2576 html += "px;"_L1;
2577 attributesEmitted = true;
2578 }
2579
2580 if (format.hasProperty(propertyId: QTextFormat::FontWeight)
2581 && format.fontWeight() != defaultCharFormat.fontWeight()) {
2582 html += " font-weight:"_L1;
2583 html += QString::number(format.fontWeight());
2584 html += u';';
2585 attributesEmitted = true;
2586 }
2587
2588 if (format.hasProperty(propertyId: QTextFormat::FontItalic)
2589 && format.fontItalic() != defaultCharFormat.fontItalic()) {
2590 html += " font-style:"_L1;
2591 html += (format.fontItalic() ? "italic"_L1 : "normal"_L1);
2592 html += u';';
2593 attributesEmitted = true;
2594 }
2595
2596 const auto decorationTag = " text-decoration:"_L1;
2597 html += decorationTag;
2598 bool hasDecoration = false;
2599 bool atLeastOneDecorationSet = false;
2600
2601 if ((format.hasProperty(propertyId: QTextFormat::FontUnderline) || format.hasProperty(propertyId: QTextFormat::TextUnderlineStyle))
2602 && format.fontUnderline() != defaultCharFormat.fontUnderline()) {
2603 hasDecoration = true;
2604 if (format.fontUnderline()) {
2605 html += " underline"_L1;
2606 atLeastOneDecorationSet = true;
2607 }
2608 }
2609
2610 if (format.hasProperty(propertyId: QTextFormat::FontOverline)
2611 && format.fontOverline() != defaultCharFormat.fontOverline()) {
2612 hasDecoration = true;
2613 if (format.fontOverline()) {
2614 html += " overline"_L1;
2615 atLeastOneDecorationSet = true;
2616 }
2617 }
2618
2619 if (format.hasProperty(propertyId: QTextFormat::FontStrikeOut)
2620 && format.fontStrikeOut() != defaultCharFormat.fontStrikeOut()) {
2621 hasDecoration = true;
2622 if (format.fontStrikeOut()) {
2623 html += " line-through"_L1;
2624 atLeastOneDecorationSet = true;
2625 }
2626 }
2627
2628 if (hasDecoration) {
2629 if (!atLeastOneDecorationSet)
2630 html += "none"_L1;
2631 html += u';';
2632 if (format.hasProperty(propertyId: QTextFormat::TextUnderlineColor)) {
2633 html += " text-decoration-color:"_L1;
2634 html += colorValue(color: format.underlineColor());
2635 html += u';';
2636 }
2637 attributesEmitted = true;
2638 } else {
2639 html.chop(n: decorationTag.size());
2640 }
2641
2642 if (format.foreground() != defaultCharFormat.foreground()
2643 && format.foreground().style() != Qt::NoBrush) {
2644 QBrush brush = format.foreground();
2645 if (brush.style() == Qt::TexturePattern) {
2646 const bool isPixmap = qHasPixmapTexture(brush);
2647 const qint64 cacheKey = isPixmap ? brush.texture().cacheKey() : brush.textureImage().cacheKey();
2648
2649 html += " -qt-fg-texture-cachekey:"_L1;
2650 html += QString::number(cacheKey);
2651 html += ";"_L1;
2652 } else if (brush.style() == Qt::LinearGradientPattern
2653 || brush.style() == Qt::RadialGradientPattern
2654 || brush.style() == Qt::ConicalGradientPattern) {
2655 const QGradient *gradient = brush.gradient();
2656 if (gradient->type() == QGradient::LinearGradient) {
2657 const QLinearGradient *linearGradient = static_cast<const QLinearGradient *>(brush.gradient());
2658
2659 html += " -qt-foreground: qlineargradient("_L1;
2660 html += "x1:"_L1 + QString::number(linearGradient->start().x()) + u',';
2661 html += "y1:"_L1 + QString::number(linearGradient->start().y()) + u',';
2662 html += "x2:"_L1 + QString::number(linearGradient->finalStop().x()) + u',';
2663 html += "y2:"_L1 + QString::number(linearGradient->finalStop().y()) + u',';
2664 } else if (gradient->type() == QGradient::RadialGradient) {
2665 const QRadialGradient *radialGradient = static_cast<const QRadialGradient *>(brush.gradient());
2666
2667 html += " -qt-foreground: qradialgradient("_L1;
2668 html += "cx:"_L1 + QString::number(radialGradient->center().x()) + u',';
2669 html += "cy:"_L1 + QString::number(radialGradient->center().y()) + u',';
2670 html += "fx:"_L1 + QString::number(radialGradient->focalPoint().x()) + u',';
2671 html += "fy:"_L1 + QString::number(radialGradient->focalPoint().y()) + u',';
2672 html += "radius:"_L1 + QString::number(radialGradient->radius()) + u',';
2673 } else {
2674 const QConicalGradient *conicalGradient = static_cast<const QConicalGradient *>(brush.gradient());
2675
2676 html += " -qt-foreground: qconicalgradient("_L1;
2677 html += "cx:"_L1 + QString::number(conicalGradient->center().x()) + u',';
2678 html += "cy:"_L1 + QString::number(conicalGradient->center().y()) + u',';
2679 html += "angle:"_L1 + QString::number(conicalGradient->angle()) + u',';
2680 }
2681
2682 const QStringList coordinateModes = { "logical"_L1, "stretchtodevice"_L1, "objectbounding"_L1, "object"_L1 };
2683 html += "coordinatemode:"_L1;
2684 html += coordinateModes.at(i: int(gradient->coordinateMode()));
2685 html += u',';
2686
2687 const QStringList spreads = { "pad"_L1, "reflect"_L1, "repeat"_L1 };
2688 html += "spread:"_L1;
2689 html += spreads.at(i: int(gradient->spread()));
2690
2691 for (const QGradientStop &stop : gradient->stops()) {
2692 html += ",stop:"_L1;
2693 html += QString::number(stop.first);
2694 html += u' ';
2695 html += colorValue(color: stop.second);
2696 }
2697
2698 html += ");"_L1;
2699 } else {
2700 html += " color:"_L1;
2701 html += colorValue(color: brush.color());
2702 html += u';';
2703 }
2704 attributesEmitted = true;
2705 }
2706
2707 if (format.background() != defaultCharFormat.background()
2708 && format.background().style() == Qt::SolidPattern) {
2709 html += " background-color:"_L1;
2710 html += colorValue(color: format.background().color());
2711 html += u';';
2712 attributesEmitted = true;
2713 }
2714
2715 if (format.verticalAlignment() != defaultCharFormat.verticalAlignment()
2716 && format.verticalAlignment() != QTextCharFormat::AlignNormal)
2717 {
2718 html += " vertical-align:"_L1;
2719
2720 QTextCharFormat::VerticalAlignment valign = format.verticalAlignment();
2721 if (valign == QTextCharFormat::AlignSubScript)
2722 html += "sub"_L1;
2723 else if (valign == QTextCharFormat::AlignSuperScript)
2724 html += "super"_L1;
2725 else if (valign == QTextCharFormat::AlignMiddle)
2726 html += "middle"_L1;
2727 else if (valign == QTextCharFormat::AlignTop)
2728 html += "top"_L1;
2729 else if (valign == QTextCharFormat::AlignBottom)
2730 html += "bottom"_L1;
2731
2732 html += u';';
2733 attributesEmitted = true;
2734 }
2735
2736 if (format.fontCapitalization() != QFont::MixedCase) {
2737 const QFont::Capitalization caps = format.fontCapitalization();
2738 if (caps == QFont::AllUppercase)
2739 html += " text-transform:uppercase;"_L1;
2740 else if (caps == QFont::AllLowercase)
2741 html += " text-transform:lowercase;"_L1;
2742 else if (caps == QFont::SmallCaps)
2743 html += " font-variant:small-caps;"_L1;
2744 attributesEmitted = true;
2745 }
2746
2747 if (format.fontWordSpacing() != 0.0) {
2748 html += " word-spacing:"_L1;
2749 html += QString::number(format.fontWordSpacing());
2750 html += "px;"_L1;
2751 attributesEmitted = true;
2752 }
2753
2754 if (format.hasProperty(propertyId: QTextFormat::TextOutline)) {
2755 QPen outlinePen = format.textOutline();
2756 html += " -qt-stroke-color:"_L1;
2757 html += colorValue(color: outlinePen.color());
2758 html += u';';
2759
2760 html += " -qt-stroke-width:"_L1;
2761 html += QString::number(outlinePen.widthF());
2762 html += "px;"_L1;
2763
2764 html += " -qt-stroke-linecap:"_L1;
2765 if (outlinePen.capStyle() == Qt::SquareCap)
2766 html += "squarecap;"_L1;
2767 else if (outlinePen.capStyle() == Qt::FlatCap)
2768 html += "flatcap;"_L1;
2769 else if (outlinePen.capStyle() == Qt::RoundCap)
2770 html += "roundcap;"_L1;
2771
2772 html += " -qt-stroke-linejoin:"_L1;
2773 if (outlinePen.joinStyle() == Qt::MiterJoin)
2774 html += "miterjoin;"_L1;
2775 else if (outlinePen.joinStyle() == Qt::SvgMiterJoin)
2776 html += "svgmiterjoin;"_L1;
2777 else if (outlinePen.joinStyle() == Qt::BevelJoin)
2778 html += "beveljoin;"_L1;
2779 else if (outlinePen.joinStyle() == Qt::RoundJoin)
2780 html += "roundjoin;"_L1;
2781
2782 if (outlinePen.joinStyle() == Qt::MiterJoin ||
2783 outlinePen.joinStyle() == Qt::SvgMiterJoin) {
2784 html += " -qt-stroke-miterlimit:"_L1;
2785 html += QString::number(outlinePen.miterLimit());
2786 html += u';';
2787 }
2788
2789 if (outlinePen.style() == Qt::CustomDashLine && !outlinePen.dashPattern().empty()) {
2790 html += " -qt-stroke-dasharray:"_L1;
2791 QString dashArrayString;
2792 QList<qreal> dashes = outlinePen.dashPattern();
2793
2794 for (int i = 0; i < dashes.length() - 1; i++) {
2795 qreal dash = dashes[i];
2796 dashArrayString += QString::number(dash) + u',';
2797 }
2798
2799 dashArrayString += QString::number(dashes.last());
2800 html += dashArrayString;
2801 html += u';';
2802
2803 html += " -qt-stroke-dashoffset:"_L1;
2804 html += QString::number(outlinePen.dashOffset());
2805 html += u';';
2806 }
2807
2808 attributesEmitted = true;
2809 }
2810
2811 return attributesEmitted;
2812}
2813
2814void QTextHtmlExporter::emitTextLength(const char *attribute, const QTextLength &length)
2815{
2816 if (length.type() == QTextLength::VariableLength) // default
2817 return;
2818
2819 html += u' ';
2820 html += QLatin1StringView(attribute);
2821 html += "=\""_L1;
2822 html += QString::number(length.rawValue());
2823
2824 if (length.type() == QTextLength::PercentageLength)
2825 html += "%\""_L1;
2826 else
2827 html += u'\"';
2828}
2829
2830void QTextHtmlExporter::emitAlignment(Qt::Alignment align)
2831{
2832 if (align & Qt::AlignLeft)
2833 return;
2834 else if (align & Qt::AlignRight)
2835 html += " align=\"right\""_L1;
2836 else if (align & Qt::AlignHCenter)
2837 html += " align=\"center\""_L1;
2838 else if (align & Qt::AlignJustify)
2839 html += " align=\"justify\""_L1;
2840}
2841
2842void QTextHtmlExporter::emitFloatStyle(QTextFrameFormat::Position pos, StyleMode mode)
2843{
2844 if (pos == QTextFrameFormat::InFlow)
2845 return;
2846
2847 if (mode == EmitStyleTag)
2848 html += " style=\"float:"_L1;
2849 else
2850 html += " float:"_L1;
2851
2852 if (pos == QTextFrameFormat::FloatLeft)
2853 html += " left;"_L1;
2854 else if (pos == QTextFrameFormat::FloatRight)
2855 html += " right;"_L1;
2856 else
2857 Q_ASSERT_X(0, "QTextHtmlExporter::emitFloatStyle()", "pos should be a valid enum type");
2858
2859 if (mode == EmitStyleTag)
2860 html += u'\"';
2861}
2862
2863static QLatin1StringView richtextBorderStyleToHtmlBorderStyle(QTextFrameFormat::BorderStyle style)
2864{
2865 switch (style) {
2866 case QTextFrameFormat::BorderStyle_None:
2867 return "none"_L1;
2868 case QTextFrameFormat::BorderStyle_Dotted:
2869 return "dotted"_L1;
2870 case QTextFrameFormat::BorderStyle_Dashed:
2871 return "dashed"_L1;
2872 case QTextFrameFormat::BorderStyle_Solid:
2873 return "solid"_L1;
2874 case QTextFrameFormat::BorderStyle_Double:
2875 return "double"_L1;
2876 case QTextFrameFormat::BorderStyle_DotDash:
2877 return "dot-dash"_L1;
2878 case QTextFrameFormat::BorderStyle_DotDotDash:
2879 return "dot-dot-dash"_L1;
2880 case QTextFrameFormat::BorderStyle_Groove:
2881 return "groove"_L1;
2882 case QTextFrameFormat::BorderStyle_Ridge:
2883 return "ridge"_L1;
2884 case QTextFrameFormat::BorderStyle_Inset:
2885 return "inset"_L1;
2886 case QTextFrameFormat::BorderStyle_Outset:
2887 return "outset"_L1;
2888 default:
2889 Q_UNREACHABLE();
2890 };
2891 return ""_L1;
2892}
2893
2894void QTextHtmlExporter::emitBorderStyle(QTextFrameFormat::BorderStyle style)
2895{
2896 Q_ASSERT(style <= QTextFrameFormat::BorderStyle_Outset);
2897
2898 html += " border-style:"_L1;
2899 html += richtextBorderStyleToHtmlBorderStyle(style);
2900 html += u';';
2901}
2902
2903void QTextHtmlExporter::emitPageBreakPolicy(QTextFormat::PageBreakFlags policy)
2904{
2905 if (policy & QTextFormat::PageBreak_AlwaysBefore)
2906 html += " page-break-before:always;"_L1;
2907
2908 if (policy & QTextFormat::PageBreak_AlwaysAfter)
2909 html += " page-break-after:always;"_L1;
2910}
2911
2912void QTextHtmlExporter::emitFontFamily(const QStringList &families)
2913{
2914 html += " font-family:"_L1;
2915
2916 bool first = true;
2917 for (const QString &family : families) {
2918 auto quote = "\'"_L1;
2919 if (family.contains(c: u'\''))
2920 quote = "&quot;"_L1;
2921
2922 if (!first)
2923 html += ","_L1;
2924 else
2925 first = false;
2926 html += quote;
2927 html += family.toHtmlEscaped();
2928 html += quote;
2929 }
2930 html += u';';
2931}
2932
2933void QTextHtmlExporter::emitMargins(const QString &top, const QString &bottom, const QString &left, const QString &right)
2934{
2935 html += " margin-top:"_L1;
2936 html += top;
2937 html += "px;"_L1;
2938
2939 html += " margin-bottom:"_L1;
2940 html += bottom;
2941 html += "px;"_L1;
2942
2943 html += " margin-left:"_L1;
2944 html += left;
2945 html += "px;"_L1;
2946
2947 html += " margin-right:"_L1;
2948 html += right;
2949 html += "px;"_L1;
2950}
2951
2952void QTextHtmlExporter::emitFragment(const QTextFragment &fragment)
2953{
2954 const QTextCharFormat format = fragment.charFormat();
2955
2956 bool closeAnchor = false;
2957
2958 if (format.isAnchor()) {
2959 const auto names = format.anchorNames();
2960 if (!names.isEmpty()) {
2961 html += "<a name=\""_L1;
2962 html += names.constFirst().toHtmlEscaped();
2963 html += "\"></a>"_L1;
2964 }
2965 const QString href = format.anchorHref();
2966 if (!href.isEmpty()) {
2967 html += "<a href=\""_L1;
2968 html += href.toHtmlEscaped();
2969 html += "\">"_L1;
2970 closeAnchor = true;
2971 }
2972 }
2973
2974 QString txt = fragment.text();
2975 const bool isObject = txt.contains(c: QChar::ObjectReplacementCharacter);
2976 const bool isImage = isObject && format.isImageFormat();
2977
2978 const auto styleTag = "<span style=\""_L1;
2979 html += styleTag;
2980
2981 bool attributesEmitted = false;
2982 if (!isImage)
2983 attributesEmitted = emitCharFormatStyle(format);
2984 if (attributesEmitted)
2985 html += "\">"_L1;
2986 else
2987 html.chop(n: styleTag.size());
2988
2989 if (isObject) {
2990 for (int i = 0; isImage && i < txt.size(); ++i) {
2991 QTextImageFormat imgFmt = format.toImageFormat();
2992
2993 html += "<img"_L1;
2994
2995 QString maxWidthCss;
2996
2997 if (imgFmt.hasProperty(propertyId: QTextFormat::ImageMaxWidth)) {
2998 auto length = imgFmt.lengthProperty(propertyId: QTextFormat::ImageMaxWidth);
2999 maxWidthCss += "max-width:"_L1;
3000 if (length.type() == QTextLength::PercentageLength)
3001 maxWidthCss += QString::number(length.rawValue()) + "%;"_L1;
3002 else if (length.type() == QTextLength::FixedLength)
3003 maxWidthCss += QString::number(length.rawValue()) + "px;"_L1;
3004 }
3005
3006 if (imgFmt.hasProperty(propertyId: QTextFormat::ImageName))
3007 emitAttribute(attribute: "src", value: imgFmt.name());
3008
3009 if (imgFmt.hasProperty(propertyId: QTextFormat::ImageAltText))
3010 emitAttribute(attribute: "alt", value: imgFmt.stringProperty(propertyId: QTextFormat::ImageAltText));
3011
3012 if (imgFmt.hasProperty(propertyId: QTextFormat::ImageTitle))
3013 emitAttribute(attribute: "title", value: imgFmt.stringProperty(propertyId: QTextFormat::ImageTitle));
3014
3015 if (imgFmt.hasProperty(propertyId: QTextFormat::ImageWidth))
3016 emitAttribute(attribute: "width", value: QString::number(imgFmt.width()));
3017
3018 if (imgFmt.hasProperty(propertyId: QTextFormat::ImageHeight))
3019 emitAttribute(attribute: "height", value: QString::number(imgFmt.height()));
3020
3021 if (imgFmt.verticalAlignment() == QTextCharFormat::AlignMiddle)
3022 html += " style=\"vertical-align: middle;"_L1 + maxWidthCss + u'\"';
3023 else if (imgFmt.verticalAlignment() == QTextCharFormat::AlignTop)
3024 html += " style=\"vertical-align: top;"_L1 + maxWidthCss + u'\"';
3025 else if (!maxWidthCss.isEmpty())
3026 html += " style=\""_L1 + maxWidthCss + u'\"';
3027
3028 if (QTextFrame *imageFrame = qobject_cast<QTextFrame *>(object: doc->objectForFormat(f: imgFmt)))
3029 emitFloatStyle(pos: imageFrame->frameFormat().position());
3030
3031 html += " />"_L1;
3032 }
3033 } else {
3034 Q_ASSERT(!txt.contains(QChar::ObjectReplacementCharacter));
3035
3036 txt = txt.toHtmlEscaped();
3037
3038 // split for [\n{LineSeparator}]
3039 // space in BR on purpose for compatibility with old-fashioned browsers
3040 txt.replace(c: u'\n', after: "<br />"_L1);
3041 txt.replace(c: QChar::LineSeparator, after: "<br />"_L1);
3042 html += txt;
3043 }
3044
3045 if (attributesEmitted)
3046 html += "</span>"_L1;
3047
3048 if (closeAnchor)
3049 html += "</a>"_L1;
3050}
3051
3052static bool isOrderedList(int style)
3053{
3054 return style == QTextListFormat::ListDecimal || style == QTextListFormat::ListLowerAlpha
3055 || style == QTextListFormat::ListUpperAlpha
3056 || style == QTextListFormat::ListUpperRoman
3057 || style == QTextListFormat::ListLowerRoman
3058 ;
3059}
3060
3061void QTextHtmlExporter::emitBlockAttributes(const QTextBlock &block)
3062{
3063 QTextBlockFormat format = block.blockFormat();
3064 emitAlignment(align: format.alignment());
3065
3066 // assume default to not bloat the html too much
3067 // html += " dir='ltr'"_L1;
3068 if (block.textDirection() == Qt::RightToLeft)
3069 html += " dir='rtl'"_L1;
3070
3071 const auto style = " style=\""_L1;
3072 html += style;
3073
3074 const bool emptyBlock = block.begin().atEnd();
3075 if (emptyBlock) {
3076 html += "-qt-paragraph-type:empty;"_L1;
3077 }
3078
3079 emitMargins(top: QString::number(format.topMargin()),
3080 bottom: QString::number(format.bottomMargin()),
3081 left: QString::number(format.leftMargin()),
3082 right: QString::number(format.rightMargin()));
3083
3084 html += " -qt-block-indent:"_L1;
3085 html += QString::number(format.indent());
3086 html += u';';
3087
3088 html += " text-indent:"_L1;
3089 html += QString::number(format.textIndent());
3090 html += "px;"_L1;
3091
3092 if (block.userState() != -1) {
3093 html += " -qt-user-state:"_L1;
3094 html += QString::number(block.userState());
3095 html += u';';
3096 }
3097
3098 if (format.lineHeightType() != QTextBlockFormat::SingleHeight) {
3099 html += " line-height:"_L1
3100 + QString::number(format.lineHeight());
3101 switch (format.lineHeightType()) {
3102 case QTextBlockFormat::ProportionalHeight:
3103 html += "%;"_L1;
3104 break;
3105 case QTextBlockFormat::FixedHeight:
3106 html += "; -qt-line-height-type: fixed;"_L1;
3107 break;
3108 case QTextBlockFormat::MinimumHeight:
3109 html += "px;"_L1;
3110 break;
3111 case QTextBlockFormat::LineDistanceHeight:
3112 html += "; -qt-line-height-type: line-distance;"_L1;
3113 break;
3114 default:
3115 html += ";"_L1;
3116 break; // Should never reach here
3117 }
3118 }
3119
3120 emitPageBreakPolicy(policy: format.pageBreakPolicy());
3121
3122 QTextCharFormat diff;
3123 if (emptyBlock) { // only print character properties when we don't expect them to be repeated by actual text in the parag
3124 const QTextCharFormat blockCharFmt = block.charFormat();
3125 diff = formatDifference(from: defaultCharFormat, to: blockCharFmt).toCharFormat();
3126 }
3127
3128 diff.clearProperty(propertyId: QTextFormat::BackgroundBrush);
3129 if (format.hasProperty(propertyId: QTextFormat::BackgroundBrush)) {
3130 QBrush bg = format.background();
3131 if (bg.style() != Qt::NoBrush)
3132 diff.setProperty(propertyId: QTextFormat::BackgroundBrush, value: format.property(propertyId: QTextFormat::BackgroundBrush));
3133 }
3134
3135 if (!diff.properties().isEmpty())
3136 emitCharFormatStyle(format: diff);
3137
3138 html += u'"';
3139
3140}
3141
3142void QTextHtmlExporter::emitBlock(const QTextBlock &block)
3143{
3144 if (block.begin().atEnd()) {
3145 // ### HACK, remove once QTextFrame::Iterator is fixed
3146 int p = block.position();
3147 if (p > 0)
3148 --p;
3149
3150 QTextDocumentPrivate::FragmentIterator frag = QTextDocumentPrivate::get(document: doc)->find(pos: p);
3151 QChar ch = QTextDocumentPrivate::get(document: doc)->buffer().at(i: frag->stringPosition);
3152 if (ch == QTextBeginningOfFrame
3153 || ch == QTextEndOfFrame)
3154 return;
3155 }
3156
3157 html += u'\n';
3158
3159 // save and later restore, in case we 'change' the default format by
3160 // emitting block char format information
3161 QTextCharFormat oldDefaultCharFormat = defaultCharFormat;
3162
3163 QTextList *list = block.textList();
3164 if (list) {
3165 if (list->itemNumber(block) == 0) { // first item? emit <ul> or appropriate
3166 const QTextListFormat format = list->format();
3167 const int style = format.style();
3168 bool ordered = false;
3169 switch (style) {
3170 case QTextListFormat::ListDisc: html += "<ul"_L1; break;
3171 case QTextListFormat::ListCircle: html += "<ul type=\"circle\""_L1; break;
3172 case QTextListFormat::ListSquare: html += "<ul type=\"square\""_L1; break;
3173 case QTextListFormat::ListDecimal: html += "<ol"_L1; ordered = true; break;
3174 case QTextListFormat::ListLowerAlpha: html += "<ol type=\"a\""_L1; ordered = true; break;
3175 case QTextListFormat::ListUpperAlpha: html += "<ol type=\"A\""_L1; ordered = true; break;
3176 case QTextListFormat::ListLowerRoman: html += "<ol type=\"i\""_L1; ordered = true; break;
3177 case QTextListFormat::ListUpperRoman: html += "<ol type=\"I\""_L1; ordered = true; break;
3178 default: html += "<ul"_L1; // ### should not happen
3179 }
3180
3181 if (ordered && format.start() != 1) {
3182 html += " start=\""_L1;
3183 html += QString::number(format.start());
3184 html += u'"';
3185 }
3186
3187 QString styleString;
3188 styleString += "margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px;"_L1;
3189
3190 if (format.hasProperty(propertyId: QTextFormat::ListIndent)) {
3191 styleString += " -qt-list-indent: "_L1;
3192 styleString += QString::number(format.indent());
3193 styleString += u';';
3194 }
3195
3196 if (format.hasProperty(propertyId: QTextFormat::ListNumberPrefix)) {
3197 QString numberPrefix = format.numberPrefix();
3198 numberPrefix.replace(c: u'"', after: "\\22"_L1);
3199 numberPrefix.replace(c: u'\'', after: "\\27"_L1); // FIXME: There's a problem in the CSS parser the prevents this from being correctly restored
3200 styleString += " -qt-list-number-prefix: "_L1;
3201 styleString += u'\'';
3202 styleString += numberPrefix;
3203 styleString += u'\'';
3204 styleString += u';';
3205 }
3206
3207 if (format.hasProperty(propertyId: QTextFormat::ListNumberSuffix)) {
3208 if (format.numberSuffix() != "."_L1) { // this is our default
3209 QString numberSuffix = format.numberSuffix();
3210 numberSuffix.replace(c: u'"', after: "\\22"_L1);
3211 numberSuffix.replace(c: u'\'', after: "\\27"_L1); // see above
3212 styleString += " -qt-list-number-suffix: "_L1;
3213 styleString += u'\'';
3214 styleString += numberSuffix;
3215 styleString += u'\'';
3216 styleString += u';';
3217 }
3218 }
3219
3220 html += " style=\""_L1;
3221 html += styleString;
3222 html += "\">\n"_L1;
3223 }
3224
3225 html += "<li"_L1;
3226
3227 const QTextCharFormat blockFmt = formatDifference(from: defaultCharFormat, to: block.charFormat()).toCharFormat();
3228 if (!blockFmt.properties().isEmpty()) {
3229 html += " style=\""_L1;
3230 emitCharFormatStyle(format: blockFmt);
3231 html += u'\"';
3232
3233 defaultCharFormat.merge(other: block.charFormat());
3234 }
3235 if (block.blockFormat().hasProperty(propertyId: QTextFormat::BlockMarker)) {
3236 switch (block.blockFormat().marker()) {
3237 case QTextBlockFormat::MarkerType::Checked:
3238 html += " class=\"checked\""_L1;
3239 break;
3240 case QTextBlockFormat::MarkerType::Unchecked:
3241 html += " class=\"unchecked\""_L1;
3242 break;
3243 case QTextBlockFormat::MarkerType::NoMarker:
3244 break;
3245 }
3246 }
3247 }
3248
3249 const QTextBlockFormat blockFormat = block.blockFormat();
3250 if (blockFormat.hasProperty(propertyId: QTextFormat::BlockTrailingHorizontalRulerWidth)) {
3251 html += "<hr"_L1;
3252
3253 QTextLength width = blockFormat.lengthProperty(propertyId: QTextFormat::BlockTrailingHorizontalRulerWidth);
3254 if (width.type() != QTextLength::VariableLength)
3255 emitTextLength(attribute: "width", length: width);
3256 html += u' ';
3257
3258 if (blockFormat.hasProperty(propertyId: QTextFormat::BackgroundBrush)) {
3259 html += "style=\""_L1;
3260 html += "background-color:"_L1;
3261 html += colorValue(color: qvariant_cast<QBrush>(v: blockFormat.property(propertyId: QTextFormat::BackgroundBrush)).color());
3262 html += u';';
3263 html += u'\"';
3264 }
3265
3266 html += "/>"_L1;
3267 return;
3268 }
3269
3270 const bool pre = blockFormat.nonBreakableLines();
3271 if (pre) {
3272 if (list)
3273 html += u'>';
3274 html += "<pre"_L1;
3275 } else if (!list) {
3276 int headingLevel = blockFormat.headingLevel();
3277 if (headingLevel > 0 && headingLevel <= 6)
3278 html += "<h"_L1 + QString::number(headingLevel);
3279 else
3280 html += "<p"_L1;
3281 }
3282
3283 emitBlockAttributes(block);
3284
3285 html += u'>';
3286 if (block.begin().atEnd())
3287 html += "<br />"_L1;
3288
3289 QTextBlock::Iterator it = block.begin();
3290 if (fragmentMarkers && !it.atEnd() && block == doc->begin())
3291 html += "<!--StartFragment-->"_L1;
3292
3293 for (; !it.atEnd(); ++it)
3294 emitFragment(fragment: it.fragment());
3295
3296 if (fragmentMarkers && block.position() + block.length() == QTextDocumentPrivate::get(document: doc)->length())
3297 html += "<!--EndFragment-->"_L1;
3298
3299 QString closeTags;
3300
3301 if (pre)
3302 html += "</pre>"_L1;
3303 else if (list)
3304 closeTags += "</li>"_L1;
3305 else {
3306 int headingLevel = blockFormat.headingLevel();
3307 if (headingLevel > 0 && headingLevel <= 6)
3308 html += QString::asprintf(format: "</h%d>", headingLevel);
3309 else
3310 html += "</p>"_L1;
3311 }
3312
3313 if (list) {
3314 if (list->itemNumber(block) == list->count() - 1) { // last item? close list
3315 if (isOrderedList(style: list->format().style()))
3316 closeTags += "</ol>"_L1;
3317 else
3318 closeTags += "</ul>"_L1;
3319 }
3320 const QTextBlock nextBlock = block.next();
3321 // If the next block is the beginning of a new deeper nested list, then we don't
3322 // want to close the current list item just yet. This should be closed when this
3323 // item is fully finished
3324 if (nextBlock.isValid() && nextBlock.textList() &&
3325 nextBlock.textList()->itemNumber(nextBlock) == 0 &&
3326 nextBlock.textList()->format().indent() > list->format().indent()) {
3327 QString lastTag;
3328 if (!closingTags.isEmpty() && list->itemNumber(block) == list->count() - 1)
3329 lastTag = closingTags.takeLast();
3330 lastTag.prepend(s: closeTags);
3331 closingTags << lastTag;
3332 } else if (list->itemNumber(block) == list->count() - 1) {
3333 // If we are at the end of the list now then we can add in the closing tags for that
3334 // current block
3335 html += closeTags;
3336 if (!closingTags.isEmpty())
3337 html += closingTags.takeLast();
3338 } else {
3339 html += closeTags;
3340 }
3341 }
3342
3343 defaultCharFormat = oldDefaultCharFormat;
3344}
3345
3346extern bool qHasPixmapTexture(const QBrush& brush);
3347
3348QString QTextHtmlExporter::findUrlForImage(const QTextDocument *doc, qint64 cacheKey, bool isPixmap)
3349{
3350 QString url;
3351 if (!doc)
3352 return url;
3353
3354 if (QTextDocument *parent = qobject_cast<QTextDocument *>(object: doc->parent()))
3355 return findUrlForImage(doc: parent, cacheKey, isPixmap);
3356
3357 const QTextDocumentPrivate *priv = QTextDocumentPrivate::get(document: doc);
3358 Q_ASSERT(priv != nullptr);
3359
3360 QMap<QUrl, QVariant>::const_iterator it = priv->cachedResources.constBegin();
3361 for (; it != priv->cachedResources.constEnd(); ++it) {
3362
3363 const QVariant &v = it.value();
3364 if (v.userType() == QMetaType::QImage && !isPixmap) {
3365 if (qvariant_cast<QImage>(v).cacheKey() == cacheKey)
3366 break;
3367 }
3368
3369 if (v.userType() == QMetaType::QPixmap && isPixmap) {
3370 if (qvariant_cast<QPixmap>(v).cacheKey() == cacheKey)
3371 break;
3372 }
3373 }
3374
3375 if (it != priv->cachedResources.constEnd())
3376 url = it.key().toString();
3377
3378 return url;
3379}
3380
3381void QTextDocumentPrivate::mergeCachedResources(const QTextDocumentPrivate *priv)
3382{
3383 if (!priv)
3384 return;
3385
3386 cachedResources.insert(map: priv->cachedResources);
3387}
3388
3389void QTextHtmlExporter::emitBackgroundAttribute(const QTextFormat &format)
3390{
3391 if (format.hasProperty(propertyId: QTextFormat::BackgroundImageUrl)) {
3392 QString url = format.property(propertyId: QTextFormat::BackgroundImageUrl).toString();
3393 emitAttribute(attribute: "background", value: url);
3394 } else {
3395 const QBrush &brush = format.background();
3396 if (brush.style() == Qt::SolidPattern) {
3397 emitAttribute(attribute: "bgcolor", value: colorValue(color: brush.color()));
3398 } else if (brush.style() == Qt::TexturePattern) {
3399 const bool isPixmap = qHasPixmapTexture(brush);
3400 const qint64 cacheKey = isPixmap ? brush.texture().cacheKey() : brush.textureImage().cacheKey();
3401
3402 const QString url = findUrlForImage(doc, cacheKey, isPixmap);
3403
3404 if (!url.isEmpty())
3405 emitAttribute(attribute: "background", value: url);
3406 }
3407 }
3408}
3409
3410void QTextHtmlExporter::emitTable(const QTextTable *table)
3411{
3412 QTextTableFormat format = table->format();
3413
3414 html += "\n<table"_L1;
3415
3416 if (format.hasProperty(propertyId: QTextFormat::FrameBorder))
3417 emitAttribute(attribute: "border", value: QString::number(format.border()));
3418
3419 emitFrameStyle(format, frameType: TableFrame);
3420
3421 emitAlignment(align: format.alignment());
3422 emitTextLength(attribute: "width", length: format.width());
3423
3424 if (format.hasProperty(propertyId: QTextFormat::TableCellSpacing))
3425 emitAttribute(attribute: "cellspacing", value: QString::number(format.cellSpacing()));
3426 if (format.hasProperty(propertyId: QTextFormat::TableCellPadding))
3427 emitAttribute(attribute: "cellpadding", value: QString::number(format.cellPadding()));
3428
3429 emitBackgroundAttribute(format);
3430
3431 html += u'>';
3432
3433 const int rows = table->rows();
3434 const int columns = table->columns();
3435
3436 QList<QTextLength> columnWidths = format.columnWidthConstraints();
3437 if (columnWidths.isEmpty()) {
3438 columnWidths.resize(size: columns);
3439 columnWidths.fill(t: QTextLength());
3440 }
3441 Q_ASSERT(columnWidths.size() == columns);
3442
3443 QVarLengthArray<bool> widthEmittedForColumn(columns);
3444 for (int i = 0; i < columns; ++i)
3445 widthEmittedForColumn[i] = false;
3446
3447 const int headerRowCount = qMin(a: format.headerRowCount(), b: rows);
3448 if (headerRowCount > 0)
3449 html += "<thead>"_L1;
3450
3451 for (int row = 0; row < rows; ++row) {
3452 html += "\n<tr>"_L1;
3453
3454 for (int col = 0; col < columns; ++col) {
3455 const QTextTableCell cell = table->cellAt(row, col);
3456
3457 // for col/rowspans
3458 if (cell.row() != row)
3459 continue;
3460
3461 if (cell.column() != col)
3462 continue;
3463
3464 html += "\n<td"_L1;
3465
3466 if (!widthEmittedForColumn[col] && cell.columnSpan() == 1) {
3467 emitTextLength(attribute: "width", length: columnWidths.at(i: col));
3468 widthEmittedForColumn[col] = true;
3469 }
3470
3471 if (cell.columnSpan() > 1)
3472 emitAttribute(attribute: "colspan", value: QString::number(cell.columnSpan()));
3473
3474 if (cell.rowSpan() > 1)
3475 emitAttribute(attribute: "rowspan", value: QString::number(cell.rowSpan()));
3476
3477 const QTextTableCellFormat cellFormat = cell.format().toTableCellFormat();
3478 emitBackgroundAttribute(format: cellFormat);
3479
3480 QTextCharFormat oldDefaultCharFormat = defaultCharFormat;
3481
3482 QTextCharFormat::VerticalAlignment valign = cellFormat.verticalAlignment();
3483
3484 QString styleString;
3485 if (valign >= QTextCharFormat::AlignMiddle && valign <= QTextCharFormat::AlignBottom) {
3486 styleString += " vertical-align:"_L1;
3487 switch (valign) {
3488 case QTextCharFormat::AlignMiddle:
3489 styleString += "middle"_L1;
3490 break;
3491 case QTextCharFormat::AlignTop:
3492 styleString += "top"_L1;
3493 break;
3494 case QTextCharFormat::AlignBottom:
3495 styleString += "bottom"_L1;
3496 break;
3497 default:
3498 break;
3499 }
3500 styleString += u';';
3501
3502 QTextCharFormat temp;
3503 temp.setVerticalAlignment(valign);
3504 defaultCharFormat.merge(other: temp);
3505 }
3506
3507 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellLeftPadding))
3508 styleString += " padding-left:"_L1 + QString::number(cellFormat.leftPadding()) + u';';
3509 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellRightPadding))
3510 styleString += " padding-right:"_L1 + QString::number(cellFormat.rightPadding()) + u';';
3511 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellTopPadding))
3512 styleString += " padding-top:"_L1 + QString::number(cellFormat.topPadding()) + u';';
3513 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellBottomPadding))
3514 styleString += " padding-bottom:"_L1 + QString::number(cellFormat.bottomPadding()) + u';';
3515
3516 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellTopBorder))
3517 styleString += " border-top:"_L1 + QString::number(cellFormat.topBorder()) + "px;"_L1;
3518 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellRightBorder))
3519 styleString += " border-right:"_L1 + QString::number(cellFormat.rightBorder()) + "px;"_L1;
3520 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellBottomBorder))
3521 styleString += " border-bottom:"_L1 + QString::number(cellFormat.bottomBorder()) + "px;"_L1;
3522 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellLeftBorder))
3523 styleString += " border-left:"_L1 + QString::number(cellFormat.leftBorder()) + "px;"_L1;
3524
3525 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellTopBorderBrush))
3526 styleString += " border-top-color:"_L1 + cellFormat.topBorderBrush().color().name() + u';';
3527 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellRightBorderBrush))
3528 styleString += " border-right-color:"_L1 + cellFormat.rightBorderBrush().color().name() + u';';
3529 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellBottomBorderBrush))
3530 styleString += " border-bottom-color:"_L1 + cellFormat.bottomBorderBrush().color().name() + u';';
3531 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellLeftBorderBrush))
3532 styleString += " border-left-color:"_L1 + cellFormat.leftBorderBrush().color().name() + u';';
3533
3534 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellTopBorderStyle))
3535 styleString += " border-top-style:"_L1 + richtextBorderStyleToHtmlBorderStyle(style: cellFormat.topBorderStyle()) + u';';
3536 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellRightBorderStyle))
3537 styleString += " border-right-style:"_L1 + richtextBorderStyleToHtmlBorderStyle(style: cellFormat.rightBorderStyle()) + u';';
3538 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellBottomBorderStyle))
3539 styleString += " border-bottom-style:"_L1 + richtextBorderStyleToHtmlBorderStyle(style: cellFormat.bottomBorderStyle()) + u';';
3540 if (cellFormat.hasProperty(propertyId: QTextFormat::TableCellLeftBorderStyle))
3541 styleString += " border-left-style:"_L1 + richtextBorderStyleToHtmlBorderStyle(style: cellFormat.leftBorderStyle()) + u';';
3542
3543 if (!styleString.isEmpty())
3544 html += " style=\""_L1 + styleString + u'\"';
3545
3546 html += u'>';
3547
3548 emitFrame(frameIt: cell.begin());
3549
3550 html += "</td>"_L1;
3551
3552 defaultCharFormat = oldDefaultCharFormat;
3553 }
3554
3555 html += "</tr>"_L1;
3556 if (headerRowCount > 0 && row == headerRowCount - 1)
3557 html += "</thead>"_L1;
3558 }
3559
3560 html += "</table>"_L1;
3561}
3562
3563void QTextHtmlExporter::emitFrame(const QTextFrame::Iterator &frameIt)
3564{
3565 if (!frameIt.atEnd()) {
3566 QTextFrame::Iterator next = frameIt;
3567 ++next;
3568 if (next.atEnd()
3569 && frameIt.currentFrame() == nullptr
3570 && frameIt.parentFrame() != doc->rootFrame()
3571 && frameIt.currentBlock().begin().atEnd())
3572 return;
3573 }
3574
3575 for (QTextFrame::Iterator it = frameIt;
3576 !it.atEnd(); ++it) {
3577 if (QTextFrame *f = it.currentFrame()) {
3578 if (QTextTable *table = qobject_cast<QTextTable *>(object: f)) {
3579 emitTable(table);
3580 } else {
3581 emitTextFrame(frame: f);
3582 }
3583 } else if (it.currentBlock().isValid()) {
3584 emitBlock(block: it.currentBlock());
3585 }
3586 }
3587}
3588
3589void QTextHtmlExporter::emitTextFrame(const QTextFrame *f)
3590{
3591 FrameType frameType = f->parentFrame() ? TextFrame : RootFrame;
3592
3593 html += "\n<table"_L1;
3594 QTextFrameFormat format = f->frameFormat();
3595
3596 if (format.hasProperty(propertyId: QTextFormat::FrameBorder))
3597 emitAttribute(attribute: "border", value: QString::number(format.border()));
3598
3599 emitFrameStyle(format, frameType);
3600
3601 emitTextLength(attribute: "width", length: format.width());
3602 emitTextLength(attribute: "height", length: format.height());
3603
3604 // root frame's bcolor goes in the <body> tag
3605 if (frameType != RootFrame)
3606 emitBackgroundAttribute(format);
3607
3608 html += u'>';
3609 html += "\n<tr>\n<td style=\"border: none;\">"_L1;
3610 emitFrame(frameIt: f->begin());
3611 html += "</td></tr></table>"_L1;
3612}
3613
3614void QTextHtmlExporter::emitFrameStyle(const QTextFrameFormat &format, FrameType frameType)
3615{
3616 const auto styleAttribute = " style=\""_L1;
3617 html += styleAttribute;
3618 const qsizetype originalHtmlLength = html.size();
3619
3620 if (frameType == TextFrame)
3621 html += "-qt-table-type: frame;"_L1;
3622 else if (frameType == RootFrame)
3623 html += "-qt-table-type: root;"_L1;
3624
3625 const QTextFrameFormat defaultFormat;
3626
3627 emitFloatStyle(pos: format.position(), mode: OmitStyleTag);
3628 emitPageBreakPolicy(policy: format.pageBreakPolicy());
3629
3630 if (format.borderBrush() != defaultFormat.borderBrush()) {
3631 html += " border-color:"_L1;
3632 html += colorValue(color: format.borderBrush().color());
3633 html += u';';
3634 }
3635
3636 if (format.borderStyle() != defaultFormat.borderStyle())
3637 emitBorderStyle(style: format.borderStyle());
3638
3639 if (format.hasProperty(propertyId: QTextFormat::FrameMargin)
3640 || format.hasProperty(propertyId: QTextFormat::FrameLeftMargin)
3641 || format.hasProperty(propertyId: QTextFormat::FrameRightMargin)
3642 || format.hasProperty(propertyId: QTextFormat::FrameTopMargin)
3643 || format.hasProperty(propertyId: QTextFormat::FrameBottomMargin))
3644 emitMargins(top: QString::number(format.topMargin()),
3645 bottom: QString::number(format.bottomMargin()),
3646 left: QString::number(format.leftMargin()),
3647 right: QString::number(format.rightMargin()));
3648
3649 if (format.property(propertyId: QTextFormat::TableBorderCollapse).toBool())
3650 html += " border-collapse:collapse;"_L1;
3651
3652 if (html.size() == originalHtmlLength) // nothing emitted?
3653 html.chop(n: styleAttribute.size());
3654 else
3655 html += u'\"';
3656}
3657
3658/*!
3659 Returns a string containing an HTML representation of the document.
3660
3661 The content of the document specifies its encoding to be UTF-8.
3662 If you later on convert the returned html string into a byte array for
3663 transmission over a network or when saving to disk you should use
3664 QString::toUtf8() to convert the string to a QByteArray.
3665
3666 \sa {Supported HTML Subset}
3667*/
3668#ifndef QT_NO_TEXTHTMLPARSER
3669QString QTextDocument::toHtml() const
3670{
3671 return QTextHtmlExporter(this).toHtml();
3672}
3673#endif // QT_NO_TEXTHTMLPARSER
3674
3675/*!
3676 \since 5.14
3677 Returns a string containing a Markdown representation of the document with
3678 the given \a features, or an empty string if writing fails for any reason.
3679
3680 \sa setMarkdown
3681*/
3682#if QT_CONFIG(textmarkdownwriter)
3683QString QTextDocument::toMarkdown(QTextDocument::MarkdownFeatures features) const
3684{
3685 QString ret;
3686 QTextStream s(&ret);
3687 QTextMarkdownWriter w(s, features);
3688 if (w.writeAll(document: this))
3689 return ret;
3690 return QString();
3691}
3692#endif
3693
3694/*!
3695 \since 5.14
3696 Replaces the entire contents of the document with the given
3697 Markdown-formatted text in the \a markdown string, with the given
3698 \a features supported. By default, all supported GitHub-style
3699 Markdown features are included; pass \c MarkdownDialectCommonMark
3700 for a more basic parse.
3701
3702 The Markdown formatting is respected as much as possible; for example,
3703 "*bold* text" will produce text where the first word has a font weight that
3704 gives it an emphasized appearance.
3705
3706 Parsing of HTML included in the \a markdown string is handled in the same
3707 way as in \l setHtml; however, Markdown formatting inside HTML blocks is
3708 not supported.
3709
3710 Some features of the parser can be enabled or disabled via the \a features
3711 argument:
3712
3713 \value MarkdownNoHTML
3714 Any HTML tags in the Markdown text will be discarded
3715 \value MarkdownDialectCommonMark
3716 The parser supports only the features standardized by CommonMark
3717 \value MarkdownDialectGitHub
3718 The parser supports the GitHub dialect
3719
3720 The default is \c MarkdownDialectGitHub.
3721
3722 The undo/redo history is reset when this function is called.
3723*/
3724#if QT_CONFIG(textmarkdownreader)
3725void QTextDocument::setMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features)
3726{
3727 QTextMarkdownImporter(this, features).import(markdown);
3728}
3729#endif
3730
3731/*!
3732 Returns a list of text formats for all the formats used in the document.
3733*/
3734QList<QTextFormat> QTextDocument::allFormats() const
3735{
3736 Q_D(const QTextDocument);
3737 return d->formatCollection()->formats;
3738}
3739
3740/*!
3741 \since 4.4
3742 \fn QTextDocument::undoCommandAdded()
3743
3744 This signal is emitted every time a new level of undo is added to the QTextDocument.
3745*/
3746
3747QT_END_NAMESPACE
3748
3749#include "moc_qtextdocument.cpp"
3750

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtbase/src/gui/text/qtextdocument.cpp