diff options
Diffstat (limited to 'src/gui/text/qtextdocument.cpp')
-rw-r--r-- | src/gui/text/qtextdocument.cpp | 226 |
1 files changed, 172 insertions, 54 deletions
diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp index 463026ed23..15a313e13d 100644 --- a/src/gui/text/qtextdocument.cpp +++ b/src/gui/text/qtextdocument.cpp @@ -11,6 +11,7 @@ #include "qtexttable.h" #include "qtextlist.h" #include <qdebug.h> +#include <qloggingcategory.h> #if QT_CONFIG(regularexpression) #include <qregularexpression.h> #endif @@ -42,6 +43,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcLayout); + using namespace Qt::StringLiterals; Q_CORE_EXPORT Q_DECL_CONST_FUNCTION unsigned int qt_int_sqrt(unsigned int n); @@ -51,6 +54,8 @@ namespace { }; /*! + \fn bool Qt::mightBeRichText(QAnyStringView text) + Returns \c true if the string \a text is likely to be rich text; otherwise returns \c false. @@ -60,21 +65,24 @@ namespace { for common cases, there is no guarantee. This function is defined in the \c <QTextDocument> header file. -*/ -bool Qt::mightBeRichText(const QString& text) + + \note In Qt versions prior to 6.7, this function took QString only. + */ +template <typename T> +static bool mightBeRichTextImpl(T text) { if (text.isEmpty()) return false; - int start = 0; + qsizetype start = 0; - while (start < text.length() && text.at(start).isSpace()) + while (start < text.size() && QChar(text.at(start)).isSpace()) ++start; // skip a leading <?xml ... ?> as for example with xhtml - if (QStringView{text}.mid(start, 5).compare("<?xml"_L1) == 0) { - while (start < text.length()) { + if (text.mid(start, 5).compare("<?xml"_L1) == 0) { + while (start < text.size()) { if (text.at(start) == u'?' - && start + 2 < text.length() + && start + 2 < text.size() && text.at(start + 1) == u'>') { start += 2; break; @@ -82,35 +90,36 @@ bool Qt::mightBeRichText(const QString& text) ++start; } - while (start < text.length() && text.at(start).isSpace()) + while (start < text.size() && QChar(text.at(start)).isSpace()) ++start; } - if (QStringView{text}.mid(start, 5).compare("<!doc"_L1, Qt::CaseInsensitive) == 0) + if (text.mid(start, 5).compare("<!doc"_L1, Qt::CaseInsensitive) == 0) return true; - int open = start; - while (open < text.length() && text.at(open) != u'<' + qsizetype open = start; + while (open < text.size() && text.at(open) != u'<' && text.at(open) != u'\n') { - if (text.at(open) == u'&' && QStringView{text}.mid(open + 1, 3) == "lt;"_L1) + if (text.at(open) == u'&' && text.mid(open + 1, 3) == "lt;"_L1) return true; // support desperate attempt of user to see <...> ++open; } - if (open < text.length() && text.at(open) == u'<') { - const int close = text.indexOf(u'>', open); + if (open < text.size() && text.at(open) == u'<') { + const qsizetype close = text.indexOf(u'>', open); if (close > -1) { - QString tag; - for (int i = open+1; i < close; ++i) { - if (text[i].isDigit() || text[i].isLetter()) - tag += text[i]; - else if (!tag.isEmpty() && text[i].isSpace()) + QVarLengthArray<char16_t> tag; + for (qsizetype i = open + 1; i < close; ++i) { + const auto current = QChar(text[i]); + if (current.isDigit() || current.isLetter()) + tag.append(current.toLower().unicode()); + else if (!tag.isEmpty() && current.isSpace()) break; - else if (!tag.isEmpty() && text[i] == u'/' && i + 1 == close) + else if (!tag.isEmpty() && current == u'/' && i + 1 == close) break; - else if (!text[i].isSpace() && (!tag.isEmpty() || text[i] != u'!')) + else if (!current.isSpace() && (!tag.isEmpty() || current != u'!')) return false; // that's not a tag } #ifndef QT_NO_TEXTHTMLPARSER - return QTextHtmlParser::lookupElement(std::move(tag).toLower()) != -1; + return QTextHtmlParser::lookupElement(tag) != -1; #else return false; #endif // QT_NO_TEXTHTMLPARSER @@ -119,6 +128,16 @@ bool Qt::mightBeRichText(const QString& text) return false; } +static bool mightBeRichTextImpl(QUtf8StringView text) +{ + return mightBeRichTextImpl(QLatin1StringView(QByteArrayView(text))); +} + +bool Qt::mightBeRichText(QAnyStringView text) +{ + return text.visit([](auto text) { return mightBeRichTextImpl(text); }); +} + /*! Converts the plain text string \a plain to an HTML-formatted paragraph while preserving most of its look. @@ -131,13 +150,13 @@ bool Qt::mightBeRichText(const QString& text) */ QString Qt::convertFromPlainText(const QString &plain, Qt::WhiteSpaceMode mode) { - int col = 0; + qsizetype col = 0; QString rich; rich += "<p>"_L1; - for (int i = 0; i < plain.length(); ++i) { + for (qsizetype i = 0; i < plain.size(); ++i) { if (plain[i] == u'\n'){ - int c = 1; - while (i+1 < plain.length() && plain[i+1] == u'\n') { + qsizetype c = 1; + while (i+1 < plain.size() && plain[i+1] == u'\n') { i++; c++; } @@ -235,7 +254,7 @@ QString Qt::convertFromPlainText(const QString &plain, Qt::WhiteSpaceMode mode) \li Text block group format changes. \endlist - \sa QTextCursor, QTextEdit, {Rich Text Processing}, {Text Object Example} + \sa QTextCursor, QTextEdit, {Rich Text Processing} */ /*! @@ -711,6 +730,8 @@ void QTextDocument::setTextWidth(qreal width) { Q_D(QTextDocument); QSizeF sz = d->pageSize; + + qCDebug(lcLayout) << "page size" << sz << "-> width" << width; sz.setWidth(width); sz.setHeight(-1); setPageSize(sz); @@ -1138,6 +1159,8 @@ QString QTextDocument::metaInformation(MetaInformation info) const return d->url; case CssMedia: return d->cssMedia; + case FrontMatter: + return d->frontMatter; } return QString(); } @@ -1161,6 +1184,9 @@ void QTextDocument::setMetaInformation(MetaInformation info, const QString &stri case CssMedia: d->cssMedia = string; break; + case FrontMatter: + d->frontMatter = string; + break; } } @@ -1200,10 +1226,18 @@ QString QTextDocument::toPlainText() const Q_D(const QTextDocument); QString txt = d->plainText(); + constexpr char16_t delims[] = { 0xfdd0, 0xfdd1, + QChar::ParagraphSeparator, QChar::LineSeparator, QChar::Nbsp }; + + const size_t pos = std::u16string_view(txt).find_first_of( + std::u16string_view(delims, std::size(delims))); + if (pos == std::u16string_view::npos) + return txt; + QChar *uc = txt.data(); - QChar *e = uc + txt.size(); + QChar *const e = uc + txt.size(); - for (; uc != e; ++uc) { + for (uc += pos; uc != e; ++uc) { switch (uc->unicode()) { case 0xfdd0: // QTextBeginningOfFrame case 0xfdd1: // QTextEndOfFrame @@ -1267,6 +1301,8 @@ void QTextDocument::setHtml(const QString &html) d->enableUndoRedo(false); d->beginEditBlock(); d->clear(); + // ctor calls parse() to build up QTextHtmlParser::nodes list + // then import() populates the QTextDocument from those QTextHtmlImporter(this, html, QTextHtmlImporter::ImportToDocument).import(); d->endEditBlock(); d->enableUndoRedo(previousState); @@ -1298,6 +1334,10 @@ void QTextDocument::setHtml(const QString &html) \value CssMedia This value is used to select the corresponding '@media' rule, if any, from a specified CSS stylesheet when setHtml() is called. This enum value has been introduced in Qt 6.3. + \value FrontMatter This value is used to select header material, if any was + extracted during parsing of the source file (currently + only from Markdown format). This enum value has been + introduced in Qt 6.8. \sa metaInformation(), setMetaInformation(), setHtml() */ @@ -1310,7 +1350,7 @@ static bool findInBlock(const QTextBlock &block, const QString &expression, int Qt::CaseSensitivity sensitivity = options & QTextDocument::FindCaseSensitively ? Qt::CaseSensitive : Qt::CaseInsensitive; int idx = -1; - while (offset >= 0 && offset <= text.length()) { + while (offset >= 0 && offset <= text.size()) { idx = (options & QTextDocument::FindBackward) ? text.lastIndexOf(expression, offset, sensitivity) : text.indexOf(expression, offset, sensitivity); if (idx == -1) @@ -1318,9 +1358,9 @@ static bool findInBlock(const QTextBlock &block, const QString &expression, int if (options & QTextDocument::FindWholeWords) { const int start = idx; - const int end = start + expression.length(); + const int end = start + expression.size(); if ((start != 0 && text.at(start - 1).isLetterOrNumber()) - || (end != text.length() && text.at(end).isLetterOrNumber())) { + || (end != text.size() && text.at(end).isLetterOrNumber())) { //if this is not a whole word, continue the search in the string offset = (options & QTextDocument::FindBackward) ? idx-1 : end+1; idx = -1; @@ -1330,7 +1370,7 @@ static bool findInBlock(const QTextBlock &block, const QString &expression, int //we have a hit, return the cursor for that. *cursor = QTextCursorPrivate::fromPosition(const_cast<QTextDocumentPrivate *>(QTextDocumentPrivate::get(block)), block.position() + idx); - cursor->setPosition(cursor->position() + expression.length(), QTextCursor::KeepAnchor); + cursor->setPosition(cursor->position() + expression.size(), QTextCursor::KeepAnchor); return true; } return false; @@ -1430,7 +1470,7 @@ static bool findInBlock(const QTextBlock &block, const QRegularExpression &expr, QRegularExpressionMatch match; int idx = -1; - while (offset >= 0 && offset <= text.length()) { + while (offset >= 0 && offset <= text.size()) { idx = (options & QTextDocument::FindBackward) ? text.lastIndexOf(expr, offset, &match) : text.indexOf(expr, offset, &match); if (idx == -1) @@ -1440,7 +1480,7 @@ static bool findInBlock(const QTextBlock &block, const QRegularExpression &expr, const int start = idx; const int end = start + match.capturedLength(); if ((start != 0 && text.at(start - 1).isLetterOrNumber()) - || (end != text.length() && text.at(end).isLetterOrNumber())) { + || (end != text.size() && text.at(end).isLetterOrNumber())) { //if this is not a whole word, continue the search in the string offset = (options & QTextDocument::FindBackward) ? idx-1 : end+1; idx = -1; @@ -1471,6 +1511,10 @@ static bool findInBlock(const QTextBlock &block, const QRegularExpression &expr, If the \a from position is 0 (the default) the search begins from the beginning of the document; otherwise it begins at the specified position. + + \warning For historical reasons, the case sensitivity option set on + \a expr is ignored. Instead, the \a options are used to determine + if the search is case sensitive or not. */ QTextCursor QTextDocument::find(const QRegularExpression &expr, int from, FindFlags options) const { @@ -1826,9 +1870,10 @@ void QTextDocument::setBaselineOffset(qreal baseline) \fn qreal QTextDocument::baselineOffset() const \since 6.0 - Returns the the baseline offset in % used in the document layout. + Returns the baseline offset in % used in the document layout. - \sa setBaselineOffset(), setSubScriptBaseline(), subScriptBaseline(), setSuperScriptBaseline(), superScriptBaseline() + \sa setBaselineOffset(), setSubScriptBaseline(), subScriptBaseline(), setSuperScriptBaseline(), + superScriptBaseline() */ qreal QTextDocument::baselineOffset() const { @@ -2235,7 +2280,8 @@ QVariant QTextDocument::loadResource(int type, const QUrl &name) int index = me->indexOfMethod("loadResource(int,QUrl)"); if (index >= 0) { QMetaMethod loader = me->method(index); - loader.invoke(p, Q_RETURN_ARG(QVariant, r), Q_ARG(int, type), Q_ARG(QUrl, name)); + // don't invoke() via a queued connection: this function needs to return a value + loader.invoke(p, Qt::DirectConnection, Q_RETURN_ARG(QVariant, r), Q_ARG(int, type), Q_ARG(QUrl, name)); } } @@ -2323,7 +2369,7 @@ static QString colorValue(QColor color) result = color.name(); } else if (color.alpha()) { QString alphaValue = QString::number(color.alphaF(), 'f', 6); - while (alphaValue.length() > 1 && alphaValue.at(alphaValue.size() - 1) == u'0') + while (alphaValue.size() > 1 && alphaValue.at(alphaValue.size() - 1) == u'0') alphaValue.chop(1); if (alphaValue.at(alphaValue.size() - 1) == u'.') alphaValue.chop(1); @@ -2364,11 +2410,14 @@ QString QTextHtmlExporter::toHtml(ExportMode mode) fragmentMarkers = (mode == ExportFragment); - html += QString::fromLatin1("<meta charset=\"utf-8\" />"); + html += "<meta charset=\"utf-8\" />"_L1; QString title = doc->metaInformation(QTextDocument::DocumentTitle); - if (!title.isEmpty()) - html += QString::fromLatin1("<title>") + title + QString::fromLatin1("</title>"); + if (!title.isEmpty()) { + html += "<title>"_L1; + html += title; + html += "</title>"_L1; + } html += "<style type=\"text/css\">\n"_L1; html += "p, li { white-space: pre-wrap; }\n"_L1; html += "hr { height: 1px; border-width: 0; }\n"_L1; @@ -2516,7 +2565,9 @@ bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format) html += u';'; attributesEmitted = true; } - } else if (format.hasProperty(QTextFormat::FontPixelSize)) { + } else if (format.hasProperty(QTextFormat::FontPixelSize) + && format.property(QTextFormat::FontPixelSize) + != defaultCharFormat.property(QTextFormat::FontPixelSize)) { html += " font-size:"_L1; html += QString::number(format.intProperty(QTextFormat::FontPixelSize)); html += "px;"_L1; @@ -2595,6 +2646,53 @@ bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format) html += " -qt-fg-texture-cachekey:"_L1; html += QString::number(cacheKey); html += ";"_L1; + } else if (brush.style() == Qt::LinearGradientPattern + || brush.style() == Qt::RadialGradientPattern + || brush.style() == Qt::ConicalGradientPattern) { + const QGradient *gradient = brush.gradient(); + if (gradient->type() == QGradient::LinearGradient) { + const QLinearGradient *linearGradient = static_cast<const QLinearGradient *>(brush.gradient()); + + html += " -qt-foreground: qlineargradient("_L1; + html += "x1:"_L1 + QString::number(linearGradient->start().x()) + u','; + html += "y1:"_L1 + QString::number(linearGradient->start().y()) + u','; + html += "x2:"_L1 + QString::number(linearGradient->finalStop().x()) + u','; + html += "y2:"_L1 + QString::number(linearGradient->finalStop().y()) + u','; + } else if (gradient->type() == QGradient::RadialGradient) { + const QRadialGradient *radialGradient = static_cast<const QRadialGradient *>(brush.gradient()); + + html += " -qt-foreground: qradialgradient("_L1; + html += "cx:"_L1 + QString::number(radialGradient->center().x()) + u','; + html += "cy:"_L1 + QString::number(radialGradient->center().y()) + u','; + html += "fx:"_L1 + QString::number(radialGradient->focalPoint().x()) + u','; + html += "fy:"_L1 + QString::number(radialGradient->focalPoint().y()) + u','; + html += "radius:"_L1 + QString::number(radialGradient->radius()) + u','; + } else { + const QConicalGradient *conicalGradient = static_cast<const QConicalGradient *>(brush.gradient()); + + html += " -qt-foreground: qconicalgradient("_L1; + html += "cx:"_L1 + QString::number(conicalGradient->center().x()) + u','; + html += "cy:"_L1 + QString::number(conicalGradient->center().y()) + u','; + html += "angle:"_L1 + QString::number(conicalGradient->angle()) + u','; + } + + const QStringList coordinateModes = { "logical"_L1, "stretchtodevice"_L1, "objectbounding"_L1, "object"_L1 }; + html += "coordinatemode:"_L1; + html += coordinateModes.at(int(gradient->coordinateMode())); + html += u','; + + const QStringList spreads = { "pad"_L1, "reflect"_L1, "repeat"_L1 }; + html += "spread:"_L1; + html += spreads.at(int(gradient->spread())); + + for (const QGradientStop &stop : gradient->stops()) { + html += ",stop:"_L1; + html += QString::number(stop.first); + html += u' '; + html += colorValue(stop.second); + } + + html += ");"_L1; } else { html += " color:"_L1; html += colorValue(brush.color()); @@ -2650,6 +2748,18 @@ bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format) attributesEmitted = true; } + if (format.hasProperty(QTextFormat::TextOutline)) { + QPen outlinePen = format.textOutline(); + html += " -qt-stroke-color:"_L1; + html += colorValue(outlinePen.color()); + html += u';'; + + html += " -qt-stroke-width:"_L1; + html += QString::number(outlinePen.widthF()); + html += "px;"_L1; + attributesEmitted = true; + } + return attributesEmitted; } @@ -2829,7 +2939,7 @@ void QTextHtmlExporter::emitFragment(const QTextFragment &fragment) html.chop(styleTag.size()); if (isObject) { - for (int i = 0; isImage && i < txt.length(); ++i) { + for (int i = 0; isImage && i < txt.size(); ++i) { QTextImageFormat imgFmt = format.toImageFormat(); html += "<img"_L1; @@ -2994,19 +3104,27 @@ void QTextHtmlExporter::emitBlock(const QTextBlock &block) if (list->itemNumber(block) == 0) { // first item? emit <ul> or appropriate const QTextListFormat format = list->format(); const int style = format.style(); + bool ordered = false; switch (style) { - case QTextListFormat::ListDecimal: html += "<ol"_L1; break; case QTextListFormat::ListDisc: html += "<ul"_L1; break; case QTextListFormat::ListCircle: html += "<ul type=\"circle\""_L1; break; case QTextListFormat::ListSquare: html += "<ul type=\"square\""_L1; break; - case QTextListFormat::ListLowerAlpha: html += "<ol type=\"a\""_L1; break; - case QTextListFormat::ListUpperAlpha: html += "<ol type=\"A\""_L1; break; - case QTextListFormat::ListLowerRoman: html += "<ol type=\"i\""_L1; break; - case QTextListFormat::ListUpperRoman: html += "<ol type=\"I\""_L1; break; + case QTextListFormat::ListDecimal: html += "<ol"_L1; ordered = true; break; + case QTextListFormat::ListLowerAlpha: html += "<ol type=\"a\""_L1; ordered = true; break; + case QTextListFormat::ListUpperAlpha: html += "<ol type=\"A\""_L1; ordered = true; break; + case QTextListFormat::ListLowerRoman: html += "<ol type=\"i\""_L1; ordered = true; break; + case QTextListFormat::ListUpperRoman: html += "<ol type=\"I\""_L1; ordered = true; break; default: html += "<ul"_L1; // ### should not happen } - QString styleString = QString::fromLatin1("margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px;"); + if (ordered && format.start() != 1) { + html += " start=\""_L1; + html += QString::number(format.start()); + html += u'"'; + } + + QString styleString; + styleString += "margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px;"_L1; if (format.hasProperty(QTextFormat::ListIndent)) { styleString += " -qt-list-indent: "_L1; @@ -3259,7 +3377,7 @@ void QTextHtmlExporter::emitTable(const QTextTable *table) columnWidths.resize(columns); columnWidths.fill(QTextLength()); } - Q_ASSERT(columnWidths.count() == columns); + Q_ASSERT(columnWidths.size() == columns); QVarLengthArray<bool> widthEmittedForColumn(columns); for (int i = 0; i < columns; ++i) @@ -3436,7 +3554,7 @@ void QTextHtmlExporter::emitFrameStyle(const QTextFrameFormat &format, FrameType { const auto styleAttribute = " style=\""_L1; html += styleAttribute; - const int originalHtmlLength = html.length(); + const qsizetype originalHtmlLength = html.size(); if (frameType == TextFrame) html += "-qt-table-type: frame;"_L1; @@ -3470,7 +3588,7 @@ void QTextHtmlExporter::emitFrameStyle(const QTextFrameFormat &format, FrameType if (format.property(QTextFormat::TableBorderCollapse).toBool()) html += " border-collapse:collapse;"_L1; - if (html.length() == originalHtmlLength) // nothing emitted? + if (html.size() == originalHtmlLength) // nothing emitted? html.chop(styleAttribute.size()); else html += u'\"'; @@ -3545,7 +3663,7 @@ QString QTextDocument::toMarkdown(QTextDocument::MarkdownFeatures features) cons #if QT_CONFIG(textmarkdownreader) void QTextDocument::setMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features) { - QTextMarkdownImporter(features).import(this, markdown); + QTextMarkdownImporter(this, features).import(markdown); } #endif |