1 /****************************************************************************
3 ** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/
6 ** This file is part of the QtQml module of the Qt Toolkit.
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** GNU Lesser General Public License Usage
10 ** This file may be used under the terms of the GNU Lesser General Public
11 ** License version 2.1 as published by the Free Software Foundation and
12 ** appearing in the file LICENSE.LGPL included in the packaging of this
13 ** file. Please review the following information to ensure the GNU Lesser
14 ** General Public License version 2.1 requirements will be met:
15 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
17 ** In addition, as a special exception, Nokia gives you certain additional
18 ** rights. These rights are described in the Nokia Qt LGPL Exception
19 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
21 ** GNU General Public License Usage
22 ** Alternatively, this file may be used under the terms of the GNU General
23 ** Public License version 3.0 as published by the Free Software Foundation
24 ** and appearing in the file LICENSE.GPL included in the packaging of this
25 ** file. Please review the following information to ensure the GNU General
26 ** Public License version 3.0 requirements will be met:
27 ** http://www.gnu.org/copyleft/gpl.html.
30 ** Alternatively, this file may be used in accordance with the terms and
31 ** conditions contained in a signed written agreement between you and Nokia.
40 ****************************************************************************/
45 #include <QTextLayout>
48 #include "qquickstyledtext_p.h"
49 #include <QQmlContext>
52 QQuickStyledText supports few tags:
55 <strong></strong> - bold
60 <font color="color_name" size="1-7"></font>
61 <h1> to <h6> - headers
63 <ol type="">, <ul type=""> and <li> - ordered and unordered lists
64 <pre></pre> - preformated
67 The opening and closing tags must be correctly nested.
72 Q_GUI_EXPORT int qt_defaultDpi();
74 class QQuickStyledTextPrivate
77 enum ListType { Ordered, Unordered };
78 enum ListFormat { Bullet, Disc, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman };
86 QQuickStyledTextPrivate(const QString &t, QTextLayout &l,
87 QList<QQuickStyledTextImgTag*> &imgTags,
91 bool *fontSizeModified)
92 : text(t), layout(l), imgTags(&imgTags), baseFont(layout.font()), baseUrl(baseUrl), hasNewLine(false), nbImages(0), updateImagePositions(false)
93 , preFormat(false), prependSpace(false), hasSpace(true), preloadImages(preloadImages), fontSizeModified(fontSizeModified), context(context)
98 void appendText(const QString &textIn, int start, int length, QString &textOut);
99 bool parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format);
100 bool parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut);
101 void parseEntity(const QChar *&ch, const QString &textIn, QString &textOut);
102 bool parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format);
103 bool parseOrderedListAttributes(const QChar *&ch, const QString &textIn);
104 bool parseUnorderedListAttributes(const QChar *&ch, const QString &textIn);
105 bool parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format);
106 void parseImageAttributes(const QChar *&ch, const QString &textIn, QString &textOut);
107 QPair<QStringRef,QStringRef> parseAttribute(const QChar *&ch, const QString &textIn);
108 QStringRef parseValue(const QChar *&ch, const QString &textIn);
109 void setFontSize(int size, QTextCharFormat &format);
111 inline void skipSpace(const QChar *&ch) {
112 while (ch->isSpace() && !ch->isNull())
116 static QString toAlpha(int value, bool upper);
117 static QString toRoman(int value, bool upper);
121 QList<QQuickStyledTextImgTag*> *imgTags;
123 QStack<List> listStack;
127 bool updateImagePositions;
132 bool *fontSizeModified;
133 QQmlContext *context;
135 static const QChar lessThan;
136 static const QChar greaterThan;
137 static const QChar equals;
138 static const QChar singleQuote;
139 static const QChar doubleQuote;
140 static const QChar slash;
141 static const QChar ampersand;
142 static const QChar bullet;
143 static const QChar disc;
144 static const QChar square;
145 static const QChar lineFeed;
146 static const QChar space;
147 static const int tabsize = 6;
150 const QChar QQuickStyledTextPrivate::lessThan(QLatin1Char('<'));
151 const QChar QQuickStyledTextPrivate::greaterThan(QLatin1Char('>'));
152 const QChar QQuickStyledTextPrivate::equals(QLatin1Char('='));
153 const QChar QQuickStyledTextPrivate::singleQuote(QLatin1Char('\''));
154 const QChar QQuickStyledTextPrivate::doubleQuote(QLatin1Char('\"'));
155 const QChar QQuickStyledTextPrivate::slash(QLatin1Char('/'));
156 const QChar QQuickStyledTextPrivate::ampersand(QLatin1Char('&'));
157 const QChar QQuickStyledTextPrivate::bullet(0x2022);
158 const QChar QQuickStyledTextPrivate::disc(0x25e6);
159 const QChar QQuickStyledTextPrivate::square(0x25a1);
160 const QChar QQuickStyledTextPrivate::lineFeed(QLatin1Char('\n'));
161 const QChar QQuickStyledTextPrivate::space(QLatin1Char(' '));
163 QQuickStyledText::QQuickStyledText(const QString &string, QTextLayout &layout,
164 QList<QQuickStyledTextImgTag*> &imgTags,
166 QQmlContext *context,
168 bool *fontSizeModified)
169 : d(new QQuickStyledTextPrivate(string, layout, imgTags, baseUrl, context, preloadImages, fontSizeModified))
173 QQuickStyledText::~QQuickStyledText()
178 void QQuickStyledText::parse(const QString &string, QTextLayout &layout,
179 QList<QQuickStyledTextImgTag*> &imgTags,
181 QQmlContext *context,
183 bool *fontSizeModified)
185 if (string.isEmpty())
187 QQuickStyledText styledText(string, layout, imgTags, baseUrl, context, preloadImages, fontSizeModified);
188 styledText.d->parse();
191 void QQuickStyledTextPrivate::parse()
193 QList<QTextLayout::FormatRange> ranges;
194 QStack<QTextCharFormat> formatStack;
197 drawText.reserve(text.count());
199 updateImagePositions = !imgTags->isEmpty();
204 bool formatChanged = false;
206 const QChar *ch = text.constData();
207 while (!ch->isNull()) {
208 if (*ch == lessThan) {
210 appendText(text, textStart, textLength, drawText);
211 } else if (prependSpace) {
212 drawText.append(space);
213 prependSpace = false;
217 if (rangeStart != drawText.length() && formatStack.count()) {
219 QTextLayout::FormatRange formatRange;
220 formatRange.format = formatStack.top();
221 formatRange.start = rangeStart;
222 formatRange.length = drawText.length() - rangeStart;
223 ranges.append(formatRange);
224 formatChanged = false;
225 } else if (ranges.count()) {
226 ranges.last().length += drawText.length() - rangeStart;
229 rangeStart = drawText.length();
233 if (parseCloseTag(ch, text, drawText)) {
234 if (formatStack.count()) {
235 formatChanged = true;
240 QTextCharFormat format;
241 if (formatStack.count())
242 format = formatStack.top();
243 if (parseTag(ch, text, drawText, format)) {
244 formatChanged = true;
245 formatStack.push(format);
248 textStart = ch - text.constData() + 1;
250 } else if (*ch == ampersand) {
252 appendText(text, textStart, textLength, drawText);
253 parseEntity(ch, text, drawText);
254 textStart = ch - text.constData() + 1;
256 } else if (ch->isSpace()) {
258 appendText(text, textStart, textLength, drawText);
260 prependSpace = !hasSpace;
261 for (const QChar *n = ch + 1; !n->isNull() && n->isSpace(); ++n)
264 } else if (*ch == lineFeed) {
265 drawText.append(QChar(QChar::LineSeparator));
268 drawText.append(QChar(QChar::Nbsp));
271 textStart = ch - text.constData() + 1;
280 appendText(text, textStart, textLength, drawText);
281 if (rangeStart != drawText.length() && formatStack.count()) {
283 QTextLayout::FormatRange formatRange;
284 formatRange.format = formatStack.top();
285 formatRange.start = rangeStart;
286 formatRange.length = drawText.length() - rangeStart;
287 ranges.append(formatRange);
288 } else if (ranges.count()) {
289 ranges.last().length += drawText.length() - rangeStart;
293 layout.setText(drawText);
294 layout.setAdditionalFormats(ranges);
297 void QQuickStyledTextPrivate::appendText(const QString &textIn, int start, int length, QString &textOut)
300 textOut.append(space);
301 textOut.append(QStringRef(&textIn, start, length));
302 prependSpace = false;
308 // Calculates and sets the correct font size in points
309 // depending on the size multiplier and base font.
311 void QQuickStyledTextPrivate::setFontSize(int size, QTextCharFormat &format)
313 static const qreal scaling[] = { 0.7, 0.8, 1.0, 1.2, 1.5, 2.0, 2.4 };
314 if (baseFont.pointSizeF() != -1)
315 format.setFontPointSize(baseFont.pointSize() * scaling[size - 1]);
317 format.setFontPointSize(baseFont.pixelSize() * qreal(72.) / qreal(qt_defaultDpi()) * scaling[size - 1]);
318 *fontSizeModified = true;
321 bool QQuickStyledTextPrivate::parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format)
325 int tagStart = ch - textIn.constData();
327 while (!ch->isNull()) {
328 if (*ch == greaterThan) {
331 QStringRef tag(&textIn, tagStart, tagLength);
332 const QChar char0 = tag.at(0);
333 if (char0 == QLatin1Char('b')) {
334 if (tagLength == 1) {
335 format.setFontWeight(QFont::Bold);
337 } else if (tagLength == 2 && tag.at(1) == QLatin1Char('r')) {
338 textOut.append(QChar(QChar::LineSeparator));
340 prependSpace = false;
343 } else if (char0 == QLatin1Char('i')) {
344 if (tagLength == 1) {
345 format.setFontItalic(true);
348 } else if (char0 == QLatin1Char('p')) {
349 if (tagLength == 1) {
351 textOut.append(QChar::LineSeparator);
353 prependSpace = false;
354 } else if (tag == QLatin1String("pre")) {
357 textOut.append(QChar::LineSeparator);
358 format.setFontFamily(QString::fromLatin1("Courier New,courier"));
359 format.setFontFixedPitch(true);
362 } else if (char0 == QLatin1Char('u')) {
363 if (tagLength == 1) {
364 format.setFontUnderline(true);
366 } else if (tag == QLatin1String("ul")) {
369 listItem.type = Unordered;
370 listItem.format = Bullet;
371 listStack.push(listItem);
373 } else if (char0 == QLatin1Char('h') && tagLength == 2) {
374 int level = tag.at(1).digitValue();
375 if (level >= 1 && level <= 6) {
377 textOut.append(QChar::LineSeparator);
379 prependSpace = false;
380 setFontSize(7 - level, format);
381 format.setFontWeight(QFont::Bold);
384 } else if (tag == QLatin1String("strong")) {
385 format.setFontWeight(QFont::Bold);
387 } else if (tag == QLatin1String("ol")) {
390 listItem.type = Ordered;
391 listItem.format = Decimal;
392 listStack.push(listItem);
393 } else if (tag == QLatin1String("li")) {
395 textOut.append(QChar(QChar::LineSeparator));
396 if (!listStack.isEmpty()) {
397 int count = ++listStack.top().level;
398 for (int i = 0; i < listStack.size(); ++i)
399 textOut += QString(tabsize, QChar::Nbsp);
400 switch (listStack.top().format) {
402 textOut += QString::number(count) % QLatin1Char('.');
405 textOut += toAlpha(count, false) % QLatin1Char('.');
408 textOut += toAlpha(count, true) % QLatin1Char('.');
411 textOut += toRoman(count, false) % QLatin1Char('.');
414 textOut += toRoman(count, true) % QLatin1Char('.');
426 textOut += QString(2, QChar::Nbsp);
430 } else if (ch->isSpace()) {
432 QStringRef tag(&textIn, tagStart, tagLength);
433 if (tag == QLatin1String("font"))
434 return parseFontAttributes(ch, textIn, format);
435 if (tag == QLatin1String("ol")) {
436 parseOrderedListAttributes(ch, textIn);
437 return false; // doesn't modify format
439 if (tag == QLatin1String("ul")) {
440 parseUnorderedListAttributes(ch, textIn);
441 return false; // doesn't modify format
443 if (tag == QLatin1String("a")) {
444 return parseAnchorAttributes(ch, textIn, format);
446 if (tag == QLatin1String("img")) {
447 parseImageAttributes(ch, textIn, textOut);
450 if (*ch == greaterThan || ch->isNull())
452 } else if (*ch != slash) {
460 bool QQuickStyledTextPrivate::parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut)
464 int tagStart = ch - textIn.constData();
466 while (!ch->isNull()) {
467 if (*ch == greaterThan) {
470 QStringRef tag(&textIn, tagStart, tagLength);
471 const QChar char0 = tag.at(0);
473 if (char0 == QLatin1Char('b')) {
476 else if (tag.at(1) == QLatin1Char('r') && tagLength == 2)
478 } else if (char0 == QLatin1Char('i')) {
481 } else if (char0 == QLatin1Char('a')) {
484 } else if (char0 == QLatin1Char('p')) {
485 if (tagLength == 1) {
486 textOut.append(QChar::LineSeparator);
490 } else if (tag == QLatin1String("pre")) {
493 textOut.append(QChar::LineSeparator);
498 } else if (char0 == QLatin1Char('u')) {
501 else if (tag == QLatin1String("ul")) {
502 if (!listStack.isEmpty()) {
504 if (!listStack.count())
505 textOut.append(QChar::LineSeparator);
509 } else if (char0 == QLatin1Char('h') && tagLength == 2) {
510 textOut.append(QChar::LineSeparator);
514 } else if (tag == QLatin1String("font")) {
516 } else if (tag == QLatin1String("strong")) {
518 } else if (tag == QLatin1String("ol")) {
519 if (!listStack.isEmpty()) {
521 if (!listStack.count())
522 textOut.append(QChar::LineSeparator);
525 } else if (tag == QLatin1String("li")) {
529 } else if (!ch->isSpace()){
538 void QQuickStyledTextPrivate::parseEntity(const QChar *&ch, const QString &textIn, QString &textOut)
540 int entityStart = ch - textIn.constData();
541 int entityLength = 0;
542 while (!ch->isNull()) {
543 if (*ch == QLatin1Char(';')) {
544 QStringRef entity(&textIn, entityStart, entityLength);
545 if (entity == QLatin1String("gt"))
546 textOut += QChar(62);
547 else if (entity == QLatin1String("lt"))
548 textOut += QChar(60);
549 else if (entity == QLatin1String("amp"))
550 textOut += QChar(38);
558 bool QQuickStyledTextPrivate::parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format)
561 QPair<QStringRef,QStringRef> attr;
563 attr = parseAttribute(ch, textIn);
564 if (attr.first == QLatin1String("color")) {
566 format.setForeground(QColor(attr.second.toString()));
567 } else if (attr.first == QLatin1String("size")) {
569 int size = attr.second.toString().toInt();
570 if (attr.second.at(0) == QLatin1Char('-') || attr.second.at(0) == QLatin1Char('+'))
572 if (size >= 1 && size <= 7)
573 setFontSize(size, format);
575 } while (!ch->isNull() && !attr.first.isEmpty());
580 bool QQuickStyledTextPrivate::parseOrderedListAttributes(const QChar *&ch, const QString &textIn)
586 listItem.type = Ordered;
587 listItem.format = Decimal;
589 QPair<QStringRef,QStringRef> attr;
591 attr = parseAttribute(ch, textIn);
592 if (attr.first == QLatin1String("type")) {
594 if (attr.second == QLatin1String("a"))
595 listItem.format = LowerAlpha;
596 else if (attr.second == QLatin1String("A"))
597 listItem.format = UpperAlpha;
598 else if (attr.second == QLatin1String("i"))
599 listItem.format = LowerRoman;
600 else if (attr.second == QLatin1String("I"))
601 listItem.format = UpperRoman;
603 } while (!ch->isNull() && !attr.first.isEmpty());
605 listStack.push(listItem);
609 bool QQuickStyledTextPrivate::parseUnorderedListAttributes(const QChar *&ch, const QString &textIn)
615 listItem.type = Unordered;
616 listItem.format = Bullet;
618 QPair<QStringRef,QStringRef> attr;
620 attr = parseAttribute(ch, textIn);
621 if (attr.first == QLatin1String("type")) {
623 if (attr.second == QLatin1String("disc"))
624 listItem.format = Disc;
625 else if (attr.second == QLatin1String("square"))
626 listItem.format = Square;
628 } while (!ch->isNull() && !attr.first.isEmpty());
630 listStack.push(listItem);
634 bool QQuickStyledTextPrivate::parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format)
638 QPair<QStringRef,QStringRef> attr;
640 attr = parseAttribute(ch, textIn);
641 if (attr.first == QLatin1String("href")) {
642 format.setAnchorHref(attr.second.toString());
643 format.setAnchor(true);
644 format.setFontUnderline(true);
647 } while (!ch->isNull() && !attr.first.isEmpty());
652 void QQuickStyledTextPrivate::parseImageAttributes(const QChar *&ch, const QString &textIn, QString &textOut)
654 qreal imgWidth = 0.0;
656 if (!updateImagePositions) {
657 QQuickStyledTextImgTag *image = new QQuickStyledTextImgTag;
658 image->position = textOut.length() + 1;
660 QPair<QStringRef,QStringRef> attr;
662 attr = parseAttribute(ch, textIn);
663 if (attr.first == QLatin1String("src")) {
664 image->url = QUrl(attr.second.toString());
665 } else if (attr.first == QLatin1String("width")) {
666 image->size.setWidth(attr.second.toString().toInt());
667 } else if (attr.first == QLatin1String("height")) {
668 image->size.setHeight(attr.second.toString().toInt());
669 } else if (attr.first == QLatin1String("align")) {
670 if (attr.second.toString() == QLatin1String("top")) {
671 image->align = QQuickStyledTextImgTag::Top;
672 } else if (attr.second.toString() == QLatin1String("middle")) {
673 image->align = QQuickStyledTextImgTag::Middle;
676 } while (!ch->isNull() && !attr.first.isEmpty());
678 if (preloadImages && !image->size.isValid()) {
679 // if we don't know its size but the image is a local image,
680 // we load it in the pixmap cache and save its implicit size
681 // to avoid a relayout later on.
682 QUrl url = baseUrl.resolved(image->url);
683 if (url.isLocalFile()) {
684 QQuickPixmap *pix = new QQuickPixmap(context->engine(), url, image->size);
685 if (pix && pix->isReady()) {
686 image->size = pix->implicitSize();
692 imgWidth = image->size.width();
693 imgTags->append(image);
696 // if we already have a list of img tags for this text
697 // we only want to update the positions of these tags.
698 QQuickStyledTextImgTag *image = imgTags->value(nbImages);
699 image->position = textOut.length() + 1;
700 imgWidth = image->size.width();
701 QPair<QStringRef,QStringRef> attr;
703 attr = parseAttribute(ch, textIn);
704 } while (!ch->isNull() && !attr.first.isEmpty());
708 QFontMetricsF fm(layout.font());
709 QString padding(qFloor(imgWidth / fm.width(QChar::Nbsp)), QChar::Nbsp);
710 textOut += QLatin1Char(' ');
712 textOut += QLatin1Char(' ');
715 QPair<QStringRef,QStringRef> QQuickStyledTextPrivate::parseAttribute(const QChar *&ch, const QString &textIn)
719 int attrStart = ch - textIn.constData();
721 while (!ch->isNull()) {
722 if (*ch == greaterThan) {
724 } else if (*ch == equals) {
726 if (*ch != singleQuote && *ch != doubleQuote) {
727 while (*ch != greaterThan && !ch->isNull())
734 QStringRef attr(&textIn, attrStart, attrLength);
735 QStringRef val = parseValue(ch, textIn);
737 return QPair<QStringRef,QStringRef>(attr,val);
745 return QPair<QStringRef,QStringRef>();
748 QStringRef QQuickStyledTextPrivate::parseValue(const QChar *&ch, const QString &textIn)
750 int valStart = ch - textIn.constData();
752 while (*ch != singleQuote && *ch != doubleQuote && !ch->isNull()) {
760 return QStringRef(&textIn, valStart, valLength);
763 QString QQuickStyledTextPrivate::toAlpha(int value, bool upper)
765 const char baseChar = upper ? 'A' : 'a';
771 result.prepend(QChar(baseChar + (c % 26)));
777 QString QQuickStyledTextPrivate::toRoman(int value, bool upper)
779 QString result = QLatin1String("?");
780 // works for up to 4999 items
782 QByteArray romanNumeral;
784 static const char romanSymbolsLower[] = "iiivixxxlxcccdcmmmm";
785 static const char romanSymbolsUpper[] = "IIIVIXXXLXCCCDCMMMM";
786 QByteArray romanSymbols;
788 romanSymbols = QByteArray::fromRawData(romanSymbolsLower, sizeof(romanSymbolsLower));
790 romanSymbols = QByteArray::fromRawData(romanSymbolsUpper, sizeof(romanSymbolsUpper));
792 int c[] = { 1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000 };
794 for (int i = 12; i >= 0; n %= c[i], i--) {
797 int startDigit = i + (i + 3) / 4;
807 romanNumeral.append(romanSymbols.mid(startDigit, numDigits));
810 result = QString::fromLatin1(romanNumeral);