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