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 QtDeclarative 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 "qdeclarativestyledtext_p.h"
49 #include <QDeclarativeContext>
52 QDeclarativeStyledText 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 class QDeclarativeStyledTextPrivate
75 enum ListType { Ordered, Unordered };
76 enum ListFormat { Bullet, Disc, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman };
84 QDeclarativeStyledTextPrivate(const QString &t, QTextLayout &l,
85 QList<QDeclarativeStyledTextImgTag*> &imgTags,
87 QDeclarativeContext *context,
89 : text(t), layout(l), imgTags(&imgTags), baseFont(layout.font()), baseUrl(baseUrl), hasNewLine(false), nbImages(0), updateImagePositions(false)
90 , preFormat(false), prependSpace(false), hasSpace(true), preloadImages(preloadImages), context(context)
95 void appendText(const QString &textIn, int start, int length, QString &textOut);
96 bool parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format);
97 bool parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut);
98 void parseEntity(const QChar *&ch, const QString &textIn, QString &textOut);
99 bool parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format);
100 bool parseOrderedListAttributes(const QChar *&ch, const QString &textIn);
101 bool parseUnorderedListAttributes(const QChar *&ch, const QString &textIn);
102 bool parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format);
103 void parseImageAttributes(const QChar *&ch, const QString &textIn, QString &textOut);
104 QPair<QStringRef,QStringRef> parseAttribute(const QChar *&ch, const QString &textIn);
105 QStringRef parseValue(const QChar *&ch, const QString &textIn);
108 inline void skipSpace(const QChar *&ch) {
109 while (ch->isSpace() && !ch->isNull())
113 static QString toAlpha(int value, bool upper);
114 static QString toRoman(int value, bool upper);
118 QList<QDeclarativeStyledTextImgTag*> *imgTags;
120 QStack<List> listStack;
124 bool updateImagePositions;
129 QDeclarativeContext *context;
131 static const QChar lessThan;
132 static const QChar greaterThan;
133 static const QChar equals;
134 static const QChar singleQuote;
135 static const QChar doubleQuote;
136 static const QChar slash;
137 static const QChar ampersand;
138 static const QChar bullet;
139 static const QChar disc;
140 static const QChar square;
141 static const QChar lineFeed;
142 static const QChar space;
143 static const int tabsize = 6;
146 const QChar QDeclarativeStyledTextPrivate::lessThan(QLatin1Char('<'));
147 const QChar QDeclarativeStyledTextPrivate::greaterThan(QLatin1Char('>'));
148 const QChar QDeclarativeStyledTextPrivate::equals(QLatin1Char('='));
149 const QChar QDeclarativeStyledTextPrivate::singleQuote(QLatin1Char('\''));
150 const QChar QDeclarativeStyledTextPrivate::doubleQuote(QLatin1Char('\"'));
151 const QChar QDeclarativeStyledTextPrivate::slash(QLatin1Char('/'));
152 const QChar QDeclarativeStyledTextPrivate::ampersand(QLatin1Char('&'));
153 const QChar QDeclarativeStyledTextPrivate::bullet(0x2022);
154 const QChar QDeclarativeStyledTextPrivate::disc(0x25e6);
155 const QChar QDeclarativeStyledTextPrivate::square(0x25a1);
156 const QChar QDeclarativeStyledTextPrivate::lineFeed(QLatin1Char('\n'));
157 const QChar QDeclarativeStyledTextPrivate::space(QLatin1Char(' '));
159 QDeclarativeStyledText::QDeclarativeStyledText(const QString &string, QTextLayout &layout,
160 QList<QDeclarativeStyledTextImgTag*> &imgTags,
162 QDeclarativeContext *context,
164 : d(new QDeclarativeStyledTextPrivate(string, layout, imgTags, baseUrl, context, preloadImages))
168 QDeclarativeStyledText::~QDeclarativeStyledText()
173 void QDeclarativeStyledText::parse(const QString &string, QTextLayout &layout,
174 QList<QDeclarativeStyledTextImgTag*> &imgTags,
176 QDeclarativeContext *context,
179 if (string.isEmpty())
181 QDeclarativeStyledText styledText(string, layout, imgTags, baseUrl, context, preloadImages);
182 styledText.d->parse();
185 void QDeclarativeStyledTextPrivate::parse()
187 QList<QTextLayout::FormatRange> ranges;
188 QStack<QTextCharFormat> formatStack;
191 drawText.reserve(text.count());
193 updateImagePositions = !imgTags->isEmpty();
198 bool formatChanged = false;
200 const QChar *ch = text.constData();
201 while (!ch->isNull()) {
202 if (*ch == lessThan) {
204 appendText(text, textStart, textLength, drawText);
205 } else if (prependSpace) {
206 drawText.append(space);
207 prependSpace = false;
211 if (rangeStart != drawText.length() && formatStack.count()) {
213 QTextLayout::FormatRange formatRange;
214 formatRange.format = formatStack.top();
215 formatRange.start = rangeStart;
216 formatRange.length = drawText.length() - rangeStart;
217 ranges.append(formatRange);
218 formatChanged = false;
219 } else if (ranges.count()) {
220 ranges.last().length += drawText.length() - rangeStart;
223 rangeStart = drawText.length();
227 if (parseCloseTag(ch, text, drawText)) {
228 if (formatStack.count()) {
229 formatChanged = true;
234 QTextCharFormat format;
235 if (formatStack.count())
236 format = formatStack.top();
237 if (parseTag(ch, text, drawText, format)) {
238 formatChanged = true;
239 formatStack.push(format);
242 textStart = ch - text.constData() + 1;
244 } else if (*ch == ampersand) {
246 appendText(text, textStart, textLength, drawText);
247 parseEntity(ch, text, drawText);
248 textStart = ch - text.constData() + 1;
250 } else if (ch->isSpace()) {
252 appendText(text, textStart, textLength, drawText);
254 prependSpace = !hasSpace;
255 for (const QChar *n = ch + 1; !n->isNull() && n->isSpace(); ++n)
258 } else if (*ch == lineFeed) {
259 drawText.append(QChar(QChar::LineSeparator));
262 drawText.append(QChar(QChar::Nbsp));
265 textStart = ch - text.constData() + 1;
274 appendText(text, textStart, textLength, drawText);
275 if (rangeStart != drawText.length() && formatStack.count()) {
277 QTextLayout::FormatRange formatRange;
278 formatRange.format = formatStack.top();
279 formatRange.start = rangeStart;
280 formatRange.length = drawText.length() - rangeStart;
281 ranges.append(formatRange);
282 } else if (ranges.count()) {
283 ranges.last().length += drawText.length() - rangeStart;
287 layout.setText(drawText);
288 layout.setAdditionalFormats(ranges);
291 void QDeclarativeStyledTextPrivate::appendText(const QString &textIn, int start, int length, QString &textOut)
294 textOut.append(space);
295 textOut.append(QStringRef(&textIn, start, length));
296 prependSpace = false;
301 bool QDeclarativeStyledTextPrivate::parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format)
305 int tagStart = ch - textIn.constData();
307 while (!ch->isNull()) {
308 if (*ch == greaterThan) {
311 QStringRef tag(&textIn, tagStart, tagLength);
312 const QChar char0 = tag.at(0);
313 if (char0 == QLatin1Char('b')) {
314 if (tagLength == 1) {
315 format.setFontWeight(QFont::Bold);
317 } else if (tagLength == 2 && tag.at(1) == QLatin1Char('r')) {
318 textOut.append(QChar(QChar::LineSeparator));
320 prependSpace = false;
323 } else if (char0 == QLatin1Char('i')) {
324 if (tagLength == 1) {
325 format.setFontItalic(true);
328 } else if (char0 == QLatin1Char('p')) {
329 if (tagLength == 1) {
331 textOut.append(QChar::LineSeparator);
333 prependSpace = false;
334 } else if (tag == QLatin1String("pre")) {
337 textOut.append(QChar::LineSeparator);
338 format.setFontFamily(QString::fromLatin1("Courier New,courier"));
339 format.setFontFixedPitch(true);
342 } else if (char0 == QLatin1Char('u')) {
343 if (tagLength == 1) {
344 format.setFontUnderline(true);
346 } else if (tag == QLatin1String("ul")) {
349 listItem.type = Unordered;
350 listItem.format = Bullet;
351 listStack.push(listItem);
353 } else if (char0 == QLatin1Char('h') && tagLength == 2) {
354 int level = tag.at(1).digitValue();
355 if (level >= 1 && level <= 6) {
356 static const qreal scaling[] = { 2.0, 1.5, 1.2, 1.0, 0.8, 0.7 };
358 textOut.append(QChar::LineSeparator);
360 prependSpace = false;
361 format.setFontPointSize(baseFont.pointSize() * scaling[level - 1]);
362 format.setFontWeight(QFont::Bold);
365 } else if (tag == QLatin1String("strong")) {
366 format.setFontWeight(QFont::Bold);
368 } else if (tag == QLatin1String("ol")) {
371 listItem.type = Ordered;
372 listItem.format = Decimal;
373 listStack.push(listItem);
374 } else if (tag == QLatin1String("li")) {
376 textOut.append(QChar(QChar::LineSeparator));
377 if (!listStack.isEmpty()) {
378 int count = ++listStack.top().level;
379 for (int i = 0; i < listStack.size(); ++i)
380 textOut += QString(tabsize, QChar::Nbsp);
381 switch (listStack.top().format) {
383 textOut += QString::number(count) % QLatin1Char('.');
386 textOut += toAlpha(count, false) % QLatin1Char('.');
389 textOut += toAlpha(count, true) % QLatin1Char('.');
392 textOut += toRoman(count, false) % QLatin1Char('.');
395 textOut += toRoman(count, true) % QLatin1Char('.');
407 textOut += QString(2, QChar::Nbsp);
411 } else if (ch->isSpace()) {
413 QStringRef tag(&textIn, tagStart, tagLength);
414 if (tag == QLatin1String("font"))
415 return parseFontAttributes(ch, textIn, format);
416 if (tag == QLatin1String("ol")) {
417 parseOrderedListAttributes(ch, textIn);
418 return false; // doesn't modify format
420 if (tag == QLatin1String("ul")) {
421 parseUnorderedListAttributes(ch, textIn);
422 return false; // doesn't modify format
424 if (tag == QLatin1String("a")) {
425 return parseAnchorAttributes(ch, textIn, format);
427 if (tag == QLatin1String("img")) {
428 parseImageAttributes(ch, textIn, textOut);
431 if (*ch == greaterThan || ch->isNull())
433 } else if (*ch != slash) {
441 bool QDeclarativeStyledTextPrivate::parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut)
445 int tagStart = ch - textIn.constData();
447 while (!ch->isNull()) {
448 if (*ch == greaterThan) {
451 QStringRef tag(&textIn, tagStart, tagLength);
452 const QChar char0 = tag.at(0);
454 if (char0 == QLatin1Char('b')) {
457 else if (tag.at(1) == QLatin1Char('r') && tagLength == 2)
459 } else if (char0 == QLatin1Char('i')) {
462 } else if (char0 == QLatin1Char('a')) {
465 } else if (char0 == QLatin1Char('p')) {
466 if (tagLength == 1) {
467 textOut.append(QChar::LineSeparator);
471 } else if (tag == QLatin1String("pre")) {
474 textOut.append(QChar::LineSeparator);
479 } else if (char0 == QLatin1Char('u')) {
482 else if (tag == QLatin1String("ul")) {
483 if (!listStack.isEmpty()) {
485 if (!listStack.count())
486 textOut.append(QChar::LineSeparator);
490 } else if (char0 == QLatin1Char('h') && tagLength == 2) {
491 textOut.append(QChar::LineSeparator);
495 } else if (tag == QLatin1String("font")) {
497 } else if (tag == QLatin1String("strong")) {
499 } else if (tag == QLatin1String("ol")) {
500 if (!listStack.isEmpty()) {
502 if (!listStack.count())
503 textOut.append(QChar::LineSeparator);
506 } else if (tag == QLatin1String("li")) {
510 } else if (!ch->isSpace()){
519 void QDeclarativeStyledTextPrivate::parseEntity(const QChar *&ch, const QString &textIn, QString &textOut)
521 int entityStart = ch - textIn.constData();
522 int entityLength = 0;
523 while (!ch->isNull()) {
524 if (*ch == QLatin1Char(';')) {
525 QStringRef entity(&textIn, entityStart, entityLength);
526 if (entity == QLatin1String("gt"))
527 textOut += QChar(62);
528 else if (entity == QLatin1String("lt"))
529 textOut += QChar(60);
530 else if (entity == QLatin1String("amp"))
531 textOut += QChar(38);
539 bool QDeclarativeStyledTextPrivate::parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format)
542 QPair<QStringRef,QStringRef> attr;
544 attr = parseAttribute(ch, textIn);
545 if (attr.first == QLatin1String("color")) {
547 format.setForeground(QColor(attr.second.toString()));
548 } else if (attr.first == QLatin1String("size")) {
550 int size = attr.second.toString().toInt();
551 if (attr.second.at(0) == QLatin1Char('-') || attr.second.at(0) == QLatin1Char('+'))
553 if (size >= 1 && size <= 7) {
554 static const qreal scaling[] = { 0.7, 0.8, 1.0, 1.2, 1.5, 2.0, 2.4 };
555 format.setFontPointSize(baseFont.pointSize() * scaling[size-1]);
558 } while (!ch->isNull() && !attr.first.isEmpty());
563 bool QDeclarativeStyledTextPrivate::parseOrderedListAttributes(const QChar *&ch, const QString &textIn)
569 listItem.type = Ordered;
570 listItem.format = Decimal;
572 QPair<QStringRef,QStringRef> attr;
574 attr = parseAttribute(ch, textIn);
575 if (attr.first == QLatin1String("type")) {
577 if (attr.second == QLatin1String("a"))
578 listItem.format = LowerAlpha;
579 else if (attr.second == QLatin1String("A"))
580 listItem.format = UpperAlpha;
581 else if (attr.second == QLatin1String("i"))
582 listItem.format = LowerRoman;
583 else if (attr.second == QLatin1String("I"))
584 listItem.format = UpperRoman;
586 } while (!ch->isNull() && !attr.first.isEmpty());
588 listStack.push(listItem);
592 bool QDeclarativeStyledTextPrivate::parseUnorderedListAttributes(const QChar *&ch, const QString &textIn)
598 listItem.type = Unordered;
599 listItem.format = Bullet;
601 QPair<QStringRef,QStringRef> attr;
603 attr = parseAttribute(ch, textIn);
604 if (attr.first == QLatin1String("type")) {
606 if (attr.second == QLatin1String("disc"))
607 listItem.format = Disc;
608 else if (attr.second == QLatin1String("square"))
609 listItem.format = Square;
611 } while (!ch->isNull() && !attr.first.isEmpty());
613 listStack.push(listItem);
617 bool QDeclarativeStyledTextPrivate::parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format)
621 QPair<QStringRef,QStringRef> attr;
623 attr = parseAttribute(ch, textIn);
624 if (attr.first == QLatin1String("href")) {
625 format.setAnchorHref(attr.second.toString());
626 format.setAnchor(true);
627 format.setFontUnderline(true);
630 } while (!ch->isNull() && !attr.first.isEmpty());
635 void QDeclarativeStyledTextPrivate::parseImageAttributes(const QChar *&ch, const QString &textIn, QString &textOut)
637 qreal imgWidth = 0.0;
639 if (!updateImagePositions) {
640 QDeclarativeStyledTextImgTag *image = new QDeclarativeStyledTextImgTag;
641 image->position = textOut.length() + 1;
643 QPair<QStringRef,QStringRef> attr;
645 attr = parseAttribute(ch, textIn);
646 if (attr.first == QLatin1String("src")) {
647 image->url = QUrl(attr.second.toString());
648 } else if (attr.first == QLatin1String("width")) {
649 image->size.setWidth(attr.second.toString().toInt());
650 } else if (attr.first == QLatin1String("height")) {
651 image->size.setHeight(attr.second.toString().toInt());
652 } else if (attr.first == QLatin1String("align")) {
653 if (attr.second.toString() == QLatin1String("top")) {
654 image->align = QDeclarativeStyledTextImgTag::Top;
655 } else if (attr.second.toString() == QLatin1String("middle")) {
656 image->align = QDeclarativeStyledTextImgTag::Middle;
659 } while (!ch->isNull() && !attr.first.isEmpty());
661 if (preloadImages && !image->size.isValid()) {
662 // if we don't know its size but the image is a local image,
663 // we load it in the pixmap cache and save its implicit size
664 // to avoid a relayout later on.
665 QUrl url = baseUrl.resolved(image->url);
666 if (url.isLocalFile()) {
667 QDeclarativePixmap *pix = new QDeclarativePixmap(context->engine(), url, image->size);
668 if (pix && pix->isReady()) {
669 image->size = pix->implicitSize();
675 imgWidth = image->size.width();
676 imgTags->append(image);
679 // if we already have a list of img tags for this text
680 // we only want to update the positions of these tags.
681 QDeclarativeStyledTextImgTag *image = imgTags->value(nbImages);
682 image->position = textOut.length() + 1;
683 imgWidth = image->size.width();
684 QPair<QStringRef,QStringRef> attr;
686 attr = parseAttribute(ch, textIn);
687 } while (!ch->isNull() && !attr.first.isEmpty());
691 QFontMetricsF fm(layout.font());
692 QString padding(qFloor(imgWidth / fm.width(QChar::Nbsp)), QChar::Nbsp);
693 textOut += QLatin1Char(' ');
695 textOut += QLatin1Char(' ');
698 QPair<QStringRef,QStringRef> QDeclarativeStyledTextPrivate::parseAttribute(const QChar *&ch, const QString &textIn)
702 int attrStart = ch - textIn.constData();
704 while (!ch->isNull()) {
705 if (*ch == greaterThan) {
707 } else if (*ch == equals) {
709 if (*ch != singleQuote && *ch != doubleQuote) {
710 while (*ch != greaterThan && !ch->isNull())
717 QStringRef attr(&textIn, attrStart, attrLength);
718 QStringRef val = parseValue(ch, textIn);
720 return QPair<QStringRef,QStringRef>(attr,val);
728 return QPair<QStringRef,QStringRef>();
731 QStringRef QDeclarativeStyledTextPrivate::parseValue(const QChar *&ch, const QString &textIn)
733 int valStart = ch - textIn.constData();
735 while (*ch != singleQuote && *ch != doubleQuote && !ch->isNull()) {
743 return QStringRef(&textIn, valStart, valLength);
746 QString QDeclarativeStyledTextPrivate::toAlpha(int value, bool upper)
748 const char baseChar = upper ? 'A' : 'a';
754 result.prepend(QChar(baseChar + (c % 26)));
760 QString QDeclarativeStyledTextPrivate::toRoman(int value, bool upper)
762 QString result = QLatin1String("?");
763 // works for up to 4999 items
765 QByteArray romanNumeral;
767 static const char romanSymbolsLower[] = "iiivixxxlxcccdcmmmm";
768 static const char romanSymbolsUpper[] = "IIIVIXXXLXCCCDCMMMM";
769 QByteArray romanSymbols;
771 romanSymbols = QByteArray::fromRawData(romanSymbolsLower, sizeof(romanSymbolsLower));
773 romanSymbols = QByteArray::fromRawData(romanSymbolsUpper, sizeof(romanSymbolsUpper));
775 int c[] = { 1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000 };
777 for (int i = 12; i >= 0; n %= c[i], i--) {
780 int startDigit = i + (i + 3) / 4;
790 romanNumeral.append(romanSymbols.mid(startDigit, numDigits));
793 result = QString::fromLatin1(romanNumeral);