Improvements to textFormat: Text.StyledText
[profile/ivi/qtdeclarative.git] / src / declarative / util / qdeclarativestyledtext.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the QtDeclarative module of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** GNU Lesser General Public License Usage
11 ** This file may be used under the terms of the GNU Lesser General Public
12 ** License version 2.1 as published by the Free Software Foundation and
13 ** appearing in the file LICENSE.LGPL included in the packaging of this
14 ** file. Please review the following information to ensure the GNU Lesser
15 ** General Public License version 2.1 requirements will be met:
16 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
17 **
18 ** In addition, as a special exception, Nokia gives you certain additional
19 ** rights. These rights are described in the Nokia Qt LGPL Exception
20 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
21 **
22 ** GNU General Public License Usage
23 ** Alternatively, this file may be used under the terms of the GNU General
24 ** Public License version 3.0 as published by the Free Software Foundation
25 ** and appearing in the file LICENSE.GPL included in the packaging of this
26 ** file. Please review the following information to ensure the GNU General
27 ** Public License version 3.0 requirements will be met:
28 ** http://www.gnu.org/copyleft/gpl.html.
29 **
30 ** Other Usage
31 ** Alternatively, this file may be used in accordance with the terms and
32 ** conditions contained in a signed written agreement between you and Nokia.
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include <QStack>
43 #include <QVector>
44 #include <QPainter>
45 #include <QTextLayout>
46 #include <QDebug>
47 #include <qmath.h>
48 #include "private/qdeclarativestyledtext_p.h"
49
50 /*
51     QDeclarativeStyledText supports few tags:
52
53     <b></b> - bold
54     <i></i> - italic
55     <br> - new line
56     <p> - paragraph
57     <u> - underlined text
58     <font color="color_name" size="1-7"></font>
59     <h1> to <h6> - headers
60     <a href=""> - anchor
61     <ol type="">, <ul type=""> and <li> - ordered and unordered lists
62
63     The opening and closing tags must be correctly nested.
64 */
65
66 QT_BEGIN_NAMESPACE
67
68 class QDeclarativeStyledTextPrivate
69 {
70 public:
71     enum ListType { Ordered, Unordered };
72     enum ListFormat { Bullet, Disc, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman };
73
74     struct List {
75         int level;
76         ListType type;
77         ListFormat format;
78     };
79
80     QDeclarativeStyledTextPrivate(const QString &t, QTextLayout &l)
81         : text(t), layout(l), baseFont(layout.font()), hasNewLine(false)
82     {
83     }
84
85     void parse();
86     bool parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format);
87     bool parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut);
88     void parseEntity(const QChar *&ch, const QString &textIn, QString &textOut);
89     bool parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format);
90     bool parseOrderedListAttributes(const QChar *&ch, const QString &textIn);
91     bool parseUnorderedListAttributes(const QChar *&ch, const QString &textIn);
92     bool parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format);
93     QPair<QStringRef,QStringRef> parseAttribute(const QChar *&ch, const QString &textIn);
94     QStringRef parseValue(const QChar *&ch, const QString &textIn);
95
96     inline void skipSpace(const QChar *&ch) {
97         while (ch->isSpace() && !ch->isNull())
98             ++ch;
99     }
100
101     static QString toAlpha(int value, bool upper);
102     static QString toRoman(int value, bool upper);
103
104     QString text;
105     QTextLayout &layout;
106     QFont baseFont;
107     QStack<List> listStack;
108     bool hasNewLine;
109
110     static const QChar lessThan;
111     static const QChar greaterThan;
112     static const QChar equals;
113     static const QChar singleQuote;
114     static const QChar doubleQuote;
115     static const QChar slash;
116     static const QChar ampersand;
117     static const QChar bullet;
118     static const QChar disc;
119     static const QChar square;
120     static const int tabsize = 6;
121 };
122
123 const QChar QDeclarativeStyledTextPrivate::lessThan(QLatin1Char('<'));
124 const QChar QDeclarativeStyledTextPrivate::greaterThan(QLatin1Char('>'));
125 const QChar QDeclarativeStyledTextPrivate::equals(QLatin1Char('='));
126 const QChar QDeclarativeStyledTextPrivate::singleQuote(QLatin1Char('\''));
127 const QChar QDeclarativeStyledTextPrivate::doubleQuote(QLatin1Char('\"'));
128 const QChar QDeclarativeStyledTextPrivate::slash(QLatin1Char('/'));
129 const QChar QDeclarativeStyledTextPrivate::ampersand(QLatin1Char('&'));
130 const QChar QDeclarativeStyledTextPrivate::bullet(0x2022);
131 const QChar QDeclarativeStyledTextPrivate::disc(0x25e6);
132 const QChar QDeclarativeStyledTextPrivate::square(0x25a1);
133
134 QDeclarativeStyledText::QDeclarativeStyledText(const QString &string, QTextLayout &layout)
135 : d(new QDeclarativeStyledTextPrivate(string, layout))
136 {
137 }
138
139 QDeclarativeStyledText::~QDeclarativeStyledText()
140 {
141     delete d;
142 }
143
144 void QDeclarativeStyledText::parse(const QString &string, QTextLayout &layout)
145 {
146     if (string.isEmpty())
147         return;
148     QDeclarativeStyledText styledText(string, layout);
149     styledText.d->parse();
150 }
151
152 void QDeclarativeStyledTextPrivate::parse()
153 {
154     QList<QTextLayout::FormatRange> ranges;
155     QStack<QTextCharFormat> formatStack;
156
157     QString drawText;
158     drawText.reserve(text.count());
159
160     int textStart = 0;
161     int textLength = 0;
162     int rangeStart = 0;
163     const QChar *ch = text.constData();
164     while (!ch->isNull()) {
165         if (*ch == lessThan) {
166             if (textLength) {
167                 QStringRef ref = QStringRef(&text, textStart, textLength);
168                 const QChar *c = ref.constData();
169                 bool isWhiteSpace = true;
170                 for (int i = 0; isWhiteSpace && (i < textLength); ++c, ++i) {
171                     if (!c->isSpace())
172                         isWhiteSpace = false;
173                 }
174                 if (!isWhiteSpace) {
175                     drawText.append(ref);
176                     hasNewLine = false;
177                 }
178             }
179             if (rangeStart != drawText.length() && formatStack.count()) {
180                 QTextLayout::FormatRange formatRange;
181                 formatRange.format = formatStack.top();
182                 formatRange.start = rangeStart;
183                 formatRange.length = drawText.length() - rangeStart;
184                 ranges.append(formatRange);
185             }
186             rangeStart = drawText.length();
187             ++ch;
188             if (*ch == slash) {
189                 ++ch;
190                 if (parseCloseTag(ch, text, drawText)) {
191                     if (formatStack.count())
192                         formatStack.pop();
193                 }
194             } else {
195                 QTextCharFormat format;
196                 if (formatStack.count())
197                     format = formatStack.top();
198                 if (parseTag(ch, text, drawText, format))
199                     formatStack.push(format);
200             }
201             textStart = ch - text.constData() + 1;
202             textLength = 0;
203         } else if (*ch == ampersand) {
204             ++ch;
205             drawText.append(QStringRef(&text, textStart, textLength));
206             parseEntity(ch, text, drawText);
207             textStart = ch - text.constData() + 1;
208             textLength = 0;
209         } else {
210             ++textLength;
211         }
212         if (!ch->isNull())
213             ++ch;
214     }
215     if (textLength)
216         drawText.append(QStringRef(&text, textStart, textLength));
217     if (rangeStart != drawText.length() && formatStack.count()) {
218         QTextLayout::FormatRange formatRange;
219         formatRange.format = formatStack.top();
220         formatRange.start = rangeStart;
221         formatRange.length = drawText.length() - rangeStart;
222         ranges.append(formatRange);
223     }
224
225     layout.setText(drawText);
226     layout.setAdditionalFormats(ranges);
227 }
228
229 bool QDeclarativeStyledTextPrivate::parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format)
230 {
231     skipSpace(ch);
232
233     int tagStart = ch - textIn.constData();
234     int tagLength = 0;
235     while (!ch->isNull()) {
236         if (*ch == greaterThan) {
237             QStringRef tag(&textIn, tagStart, tagLength);
238             const QChar char0 = tag.at(0);
239             if (char0 == QLatin1Char('b')) {
240                 if (tagLength == 1)
241                     format.setFontWeight(QFont::Bold);
242                 else if (tagLength == 2 && tag.at(1) == QLatin1Char('r')) {
243                     textOut.append(QChar(QChar::LineSeparator));
244                     return false;
245                 }
246             } else if (char0 == QLatin1Char('i')) {
247                 if (tagLength == 1)
248                     format.setFontItalic(true);
249             } else if (char0 == QLatin1Char('p')) {
250                 if (tagLength == 1) {
251                     if (!hasNewLine)
252                         textOut.append(QChar::LineSeparator);
253                 }
254             } else if (char0 == QLatin1Char('u')) {
255                 if (tagLength == 1)
256                     format.setFontUnderline(true);
257                 else if (tag == QLatin1String("ul")) {
258                     List listItem;
259                     listItem.level = 0;
260                     listItem.type = Unordered;
261                     listItem.format = Bullet;
262                     listStack.push(listItem);
263                 }
264             } else if (char0 == QLatin1Char('h') && tagLength == 2) {
265                 int level = tag.at(1).digitValue();
266                 if (level >= 1 && level <= 6) {
267                     static const qreal scaling[] = { 2.0, 1.5, 1.2, 1.0, 0.8, 0.7 };
268                     if (!hasNewLine)
269                         textOut.append(QChar::LineSeparator);
270                     format.setFontPointSize(baseFont.pointSize() * scaling[level - 1]);
271                     format.setFontWeight(QFont::Bold);
272                 }
273             } else if (tag == QLatin1String("ol")) {
274                 List listItem;
275                 listItem.level = 0;
276                 listItem.type = Ordered;
277                 listItem.format = Decimal;
278                 listStack.push(listItem);
279             } else if (tag == QLatin1String("li")) {
280                 if (!hasNewLine)
281                     textOut.append(QChar(QChar::LineSeparator));
282                 if (!listStack.isEmpty()) {
283                     int count = ++listStack.top().level;
284                     for (int i = 0; i < listStack.size(); ++i)
285                         textOut += QString(tabsize, QChar::Nbsp);
286                     switch (listStack.top().format) {
287                     case Decimal:
288                         textOut += QString::number(count) % QLatin1Char('.');
289                         break;
290                     case LowerAlpha:
291                         textOut += toAlpha(count, false) % QLatin1Char('.');
292                         break;
293                     case UpperAlpha:
294                         textOut += toAlpha(count, true) % QLatin1Char('.');
295                         break;
296                     case LowerRoman:
297                         textOut += toRoman(count, false) % QLatin1Char('.');
298                         break;
299                     case UpperRoman:
300                         textOut += toRoman(count, true) % QLatin1Char('.');
301                         break;
302                     case Bullet:
303                         textOut += bullet;
304                         break;
305                     case Disc:
306                         textOut += disc;
307                         break;
308                     case Square:
309                         textOut += square;
310                         break;
311                     }
312                     textOut += QString(2, QChar::Nbsp);
313                 }
314             }
315             return true;
316         } else if (ch->isSpace()) {
317             // may have params.
318             QStringRef tag(&textIn, tagStart, tagLength);
319             if (tag == QLatin1String("font"))
320                 return parseFontAttributes(ch, textIn, format);
321             if (tag == QLatin1String("ol"))
322                 return parseOrderedListAttributes(ch, textIn);
323             if (tag == QLatin1String("ul"))
324                 return parseUnorderedListAttributes(ch, textIn);
325             if (tag == QLatin1String("a")) {
326                 return parseAnchorAttributes(ch, textIn, format);
327             }
328             if (*ch == greaterThan || ch->isNull())
329                 continue;
330         } else if (*ch != slash) {
331             tagLength++;
332         }
333         ++ch;
334     }
335     return false;
336 }
337
338 bool QDeclarativeStyledTextPrivate::parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut)
339 {
340     skipSpace(ch);
341
342     int tagStart = ch - textIn.constData();
343     int tagLength = 0;
344     while (!ch->isNull()) {
345         if (*ch == greaterThan) {
346             QStringRef tag(&textIn, tagStart, tagLength);
347             const QChar char0 = tag.at(0);
348             hasNewLine = false;
349             if (char0 == QLatin1Char('b')) {
350                 if (tagLength == 1)
351                     return true;
352                 else if (tag.at(1) == QLatin1Char('r') && tagLength == 2)
353                     return true;
354             } else if (char0 == QLatin1Char('i')) {
355                 if (tagLength == 1)
356                     return true;
357             } else if (char0 == QLatin1Char('a')) {
358                 if (tagLength == 1)
359                     return true;
360             } else if (char0 == QLatin1Char('p')) {
361                 if (tagLength == 1) {
362                     textOut.append(QChar::LineSeparator);
363                     hasNewLine = true;
364                     return true;
365                 }
366             } else if (char0 == QLatin1Char('u')) {
367                 if (tagLength == 1)
368                     return true;
369                 else if (tag == QLatin1String("ul")) {
370                     if (!listStack.isEmpty()) {
371                         listStack.pop();
372                         if (!listStack.count())
373                             textOut.append(QChar::LineSeparator);
374                     }
375                     return true;
376                 }
377             } else if (char0 == QLatin1Char('h') && tagLength == 2) {
378                 textOut.append(QChar::LineSeparator);
379                 hasNewLine = true;
380                 return true;
381             } else if (tag == QLatin1String("font")) {
382                 return true;
383             } else if (tag == QLatin1String("ol")) {
384                 if (!listStack.isEmpty()) {
385                     listStack.pop();
386                     if (!listStack.count())
387                         textOut.append(QChar::LineSeparator);
388                 }
389                 return true;
390             } else if (tag == QLatin1String("li")) {
391                 return true;
392             }
393             return false;
394         } else if (!ch->isSpace()){
395             tagLength++;
396         }
397         ++ch;
398     }
399
400     return false;
401 }
402
403 void QDeclarativeStyledTextPrivate::parseEntity(const QChar *&ch, const QString &textIn, QString &textOut)
404 {
405     int entityStart = ch - textIn.constData();
406     int entityLength = 0;
407     while (!ch->isNull()) {
408         if (*ch == QLatin1Char(';')) {
409             QStringRef entity(&textIn, entityStart, entityLength);
410             if (entity == QLatin1String("gt"))
411                 textOut += QChar(62);
412             else if (entity == QLatin1String("lt"))
413                 textOut += QChar(60);
414             else if (entity == QLatin1String("amp"))
415                 textOut += QChar(38);
416             return;
417         }
418         ++entityLength;
419         ++ch;
420     }
421 }
422
423 bool QDeclarativeStyledTextPrivate::parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format)
424 {
425     bool valid = false;
426     QPair<QStringRef,QStringRef> attr;
427     do {
428         attr = parseAttribute(ch, textIn);
429         if (attr.first == QLatin1String("color")) {
430             valid = true;
431             format.setForeground(QColor(attr.second.toString()));
432         } else if (attr.first == QLatin1String("size")) {
433             valid = true;
434             int size = attr.second.toString().toInt();
435             if (attr.second.at(0) == QLatin1Char('-') || attr.second.at(0) == QLatin1Char('+'))
436                 size += 3;
437             if (size >= 1 && size <= 7) {
438                 static const qreal scaling[] = { 0.7, 0.8, 1.0, 1.2, 1.5, 2.0, 2.4 };
439                 format.setFontPointSize(baseFont.pointSize() * scaling[size-1]);
440             }
441         }
442     } while (!ch->isNull() && !attr.first.isEmpty());
443
444     return valid;
445 }
446
447 bool QDeclarativeStyledTextPrivate::parseOrderedListAttributes(const QChar *&ch, const QString &textIn)
448 {
449     bool valid = false;
450
451     List listItem;
452     listItem.level = 0;
453     listItem.type = Ordered;
454     listItem.format = Decimal;
455
456     QPair<QStringRef,QStringRef> attr;
457     do {
458         attr = parseAttribute(ch, textIn);
459         if (attr.first == QLatin1String("type")) {
460             valid = true;
461             if (attr.second == QLatin1String("a"))
462                 listItem.format = LowerAlpha;
463             else if (attr.second == QLatin1String("A"))
464                 listItem.format = UpperAlpha;
465             else if (attr.second == QLatin1String("i"))
466                 listItem.format = LowerRoman;
467             else if (attr.second == QLatin1String("I"))
468                 listItem.format = UpperRoman;
469         }
470     } while (!ch->isNull() && !attr.first.isEmpty());
471
472     listStack.push(listItem);
473     return valid;
474 }
475
476 bool QDeclarativeStyledTextPrivate::parseUnorderedListAttributes(const QChar *&ch, const QString &textIn)
477 {
478     bool valid = false;
479
480     List listItem;
481     listItem.level = 0;
482     listItem.type = Unordered;
483     listItem.format = Bullet;
484
485     QPair<QStringRef,QStringRef> attr;
486     do {
487         attr = parseAttribute(ch, textIn);
488         if (attr.first == QLatin1String("type")) {
489             valid = true;
490             if (attr.second == QLatin1String("disc"))
491                 listItem.format = Disc;
492             else if (attr.second == QLatin1String("square"))
493                 listItem.format = Square;
494         }
495     } while (!ch->isNull() && !attr.first.isEmpty());
496
497     listStack.push(listItem);
498     return valid;
499 }
500
501 bool QDeclarativeStyledTextPrivate::parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format)
502 {
503     bool valid = false;
504
505     QPair<QStringRef,QStringRef> attr;
506     do {
507         attr = parseAttribute(ch, textIn);
508         if (attr.first == QLatin1String("href")) {
509             format.setAnchorHref(attr.second.toString());
510             format.setAnchor(true);
511             format.setFontUnderline(true);
512             format.setForeground(QColor("blue"));
513             valid = true;
514         }
515     } while (!ch->isNull() && !attr.first.isEmpty());
516
517     return valid;
518 }
519
520 QPair<QStringRef,QStringRef> QDeclarativeStyledTextPrivate::parseAttribute(const QChar *&ch, const QString &textIn)
521 {
522     skipSpace(ch);
523
524     int attrStart = ch - textIn.constData();
525     int attrLength = 0;
526     while (!ch->isNull()) {
527         if (*ch == greaterThan) {
528             break;
529         } else if (*ch == equals) {
530             ++ch;
531             if (*ch != singleQuote && *ch != doubleQuote) {
532                 while (*ch != greaterThan && !ch->isNull())
533                     ++ch;
534                 break;
535             }
536             ++ch;
537             if (!attrLength)
538                 break;
539             QStringRef attr(&textIn, attrStart, attrLength);
540             QStringRef val = parseValue(ch, textIn);
541             if (!val.isEmpty())
542                 return QPair<QStringRef,QStringRef>(attr,val);
543             break;
544         } else {
545             ++attrLength;
546         }
547         ++ch;
548     }
549
550     return QPair<QStringRef,QStringRef>();
551 }
552
553 QStringRef QDeclarativeStyledTextPrivate::parseValue(const QChar *&ch, const QString &textIn)
554 {
555     int valStart = ch - textIn.constData();
556     int valLength = 0;
557     while (*ch != singleQuote && *ch != doubleQuote && !ch->isNull()) {
558         ++valLength;
559         ++ch;
560     }
561     if (ch->isNull())
562         return QStringRef();
563     ++ch; // skip quote
564
565     return QStringRef(&textIn, valStart, valLength);
566 }
567
568 QString QDeclarativeStyledTextPrivate::toAlpha(int value, bool upper)
569 {
570     const char baseChar = upper ? 'A' : 'a';
571
572     QString result;
573     int c = value;
574     while (c > 0) {
575         c--;
576         result.prepend(QChar(baseChar + (c % 26)));
577         c /= 26;
578     }
579     return result;
580 }
581
582 QString QDeclarativeStyledTextPrivate::toRoman(int value, bool upper)
583 {
584     QString result = QLatin1String("?");
585     // works for up to 4999 items
586     if (value < 5000) {
587         QByteArray romanNumeral;
588
589         static const char romanSymbolsLower[] = "iiivixxxlxcccdcmmmm";
590         static const char romanSymbolsUpper[] = "IIIVIXXXLXCCCDCMMMM";
591         QByteArray romanSymbols;
592         if (!upper)
593             romanSymbols = QByteArray::fromRawData(romanSymbolsLower, sizeof(romanSymbolsLower));
594         else
595             romanSymbols = QByteArray::fromRawData(romanSymbolsUpper, sizeof(romanSymbolsUpper));
596
597         int c[] = { 1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000 };
598         int n = value;
599         for (int i = 12; i >= 0; n %= c[i], i--) {
600             int q = n / c[i];
601             if (q > 0) {
602                 int startDigit = i + (i + 3) / 4;
603                 int numDigits;
604                 if (i % 4) {
605                     if ((i - 2) % 4)
606                         numDigits = 2;
607                     else
608                         numDigits = 1;
609                 }
610                 else
611                     numDigits = q;
612                 romanNumeral.append(romanSymbols.mid(startDigit, numDigits));
613             }
614         }
615         result = QString::fromLatin1(romanNumeral);
616     }
617     return result;
618 }
619
620 QT_END_NAMESPACE