diff options
Diffstat (limited to 'src/quick/util/qdeclarativestyledtext.cpp')
-rw-r--r-- | src/quick/util/qdeclarativestyledtext.cpp | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/src/quick/util/qdeclarativestyledtext.cpp b/src/quick/util/qdeclarativestyledtext.cpp new file mode 100644 index 0000000000..1c7ca5fbfd --- /dev/null +++ b/src/quick/util/qdeclarativestyledtext.cpp @@ -0,0 +1,624 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the QtDeclarative module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** GNU Lesser General Public License Usage +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this +** file. Please review the following information to ensure the GNU Lesser +** General Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QStack> +#include <QVector> +#include <QPainter> +#include <QTextLayout> +#include <QDebug> +#include <qmath.h> +#include "qdeclarativestyledtext_p.h" + +/* + QDeclarativeStyledText supports few tags: + + <b></b> - bold + <i></i> - italic + <br> - new line + <p> - paragraph + <u> - underlined text + <font color="color_name" size="1-7"></font> + <h1> to <h6> - headers + <a href=""> - anchor + <ol type="">, <ul type=""> and <li> - ordered and unordered lists + + The opening and closing tags must be correctly nested. +*/ + +QT_BEGIN_NAMESPACE + +class QDeclarativeStyledTextPrivate +{ +public: + enum ListType { Ordered, Unordered }; + enum ListFormat { Bullet, Disc, Square, Decimal, LowerAlpha, UpperAlpha, LowerRoman, UpperRoman }; + + struct List { + int level; + ListType type; + ListFormat format; + }; + + QDeclarativeStyledTextPrivate(const QString &t, QTextLayout &l) + : text(t), layout(l), baseFont(layout.font()), hasNewLine(false) + { + } + + void parse(); + bool parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format); + bool parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut); + void parseEntity(const QChar *&ch, const QString &textIn, QString &textOut); + bool parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format); + bool parseOrderedListAttributes(const QChar *&ch, const QString &textIn); + bool parseUnorderedListAttributes(const QChar *&ch, const QString &textIn); + bool parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format); + QPair<QStringRef,QStringRef> parseAttribute(const QChar *&ch, const QString &textIn); + QStringRef parseValue(const QChar *&ch, const QString &textIn); + + inline void skipSpace(const QChar *&ch) { + while (ch->isSpace() && !ch->isNull()) + ++ch; + } + + static QString toAlpha(int value, bool upper); + static QString toRoman(int value, bool upper); + + QString text; + QTextLayout &layout; + QFont baseFont; + QStack<List> listStack; + bool hasNewLine; + + static const QChar lessThan; + static const QChar greaterThan; + static const QChar equals; + static const QChar singleQuote; + static const QChar doubleQuote; + static const QChar slash; + static const QChar ampersand; + static const QChar bullet; + static const QChar disc; + static const QChar square; + static const int tabsize = 6; +}; + +const QChar QDeclarativeStyledTextPrivate::lessThan(QLatin1Char('<')); +const QChar QDeclarativeStyledTextPrivate::greaterThan(QLatin1Char('>')); +const QChar QDeclarativeStyledTextPrivate::equals(QLatin1Char('=')); +const QChar QDeclarativeStyledTextPrivate::singleQuote(QLatin1Char('\'')); +const QChar QDeclarativeStyledTextPrivate::doubleQuote(QLatin1Char('\"')); +const QChar QDeclarativeStyledTextPrivate::slash(QLatin1Char('/')); +const QChar QDeclarativeStyledTextPrivate::ampersand(QLatin1Char('&')); +const QChar QDeclarativeStyledTextPrivate::bullet(0x2022); +const QChar QDeclarativeStyledTextPrivate::disc(0x25e6); +const QChar QDeclarativeStyledTextPrivate::square(0x25a1); + +QDeclarativeStyledText::QDeclarativeStyledText(const QString &string, QTextLayout &layout) +: d(new QDeclarativeStyledTextPrivate(string, layout)) +{ +} + +QDeclarativeStyledText::~QDeclarativeStyledText() +{ + delete d; +} + +void QDeclarativeStyledText::parse(const QString &string, QTextLayout &layout) +{ + if (string.isEmpty()) + return; + QDeclarativeStyledText styledText(string, layout); + styledText.d->parse(); +} + +void QDeclarativeStyledTextPrivate::parse() +{ + QList<QTextLayout::FormatRange> ranges; + QStack<QTextCharFormat> formatStack; + + QString drawText; + drawText.reserve(text.count()); + + int textStart = 0; + int textLength = 0; + int rangeStart = 0; + const QChar *ch = text.constData(); + while (!ch->isNull()) { + if (*ch == lessThan) { + if (textLength) { + QStringRef ref = QStringRef(&text, textStart, textLength); + const QChar *c = ref.constData(); + bool isWhiteSpace = true; + for (int i = 0; isWhiteSpace && (i < textLength); ++c, ++i) { + if (!c->isSpace()) + isWhiteSpace = false; + } + if (!isWhiteSpace) { + drawText.append(ref); + hasNewLine = false; + } + } + if (rangeStart != drawText.length() && formatStack.count()) { + QTextLayout::FormatRange formatRange; + formatRange.format = formatStack.top(); + formatRange.start = rangeStart; + formatRange.length = drawText.length() - rangeStart; + ranges.append(formatRange); + } + rangeStart = drawText.length(); + ++ch; + if (*ch == slash) { + ++ch; + if (parseCloseTag(ch, text, drawText)) { + if (formatStack.count()) + formatStack.pop(); + } + } else { + QTextCharFormat format; + if (formatStack.count()) + format = formatStack.top(); + if (parseTag(ch, text, drawText, format)) + formatStack.push(format); + } + textStart = ch - text.constData() + 1; + textLength = 0; + } else if (*ch == ampersand) { + ++ch; + drawText.append(QStringRef(&text, textStart, textLength)); + parseEntity(ch, text, drawText); + textStart = ch - text.constData() + 1; + textLength = 0; + } else { + ++textLength; + } + if (!ch->isNull()) + ++ch; + } + if (textLength) + drawText.append(QStringRef(&text, textStart, textLength)); + if (rangeStart != drawText.length() && formatStack.count()) { + QTextLayout::FormatRange formatRange; + formatRange.format = formatStack.top(); + formatRange.start = rangeStart; + formatRange.length = drawText.length() - rangeStart; + ranges.append(formatRange); + } + + layout.setText(drawText); + layout.setAdditionalFormats(ranges); +} + +bool QDeclarativeStyledTextPrivate::parseTag(const QChar *&ch, const QString &textIn, QString &textOut, QTextCharFormat &format) +{ + skipSpace(ch); + + int tagStart = ch - textIn.constData(); + int tagLength = 0; + while (!ch->isNull()) { + if (*ch == greaterThan) { + if (tagLength == 0) + return false; + QStringRef tag(&textIn, tagStart, tagLength); + const QChar char0 = tag.at(0); + if (char0 == QLatin1Char('b')) { + if (tagLength == 1) + format.setFontWeight(QFont::Bold); + else if (tagLength == 2 && tag.at(1) == QLatin1Char('r')) { + textOut.append(QChar(QChar::LineSeparator)); + return false; + } + } else if (char0 == QLatin1Char('i')) { + if (tagLength == 1) + format.setFontItalic(true); + } else if (char0 == QLatin1Char('p')) { + if (tagLength == 1) { + if (!hasNewLine) + textOut.append(QChar::LineSeparator); + } + } else if (char0 == QLatin1Char('u')) { + if (tagLength == 1) + format.setFontUnderline(true); + else if (tag == QLatin1String("ul")) { + List listItem; + listItem.level = 0; + listItem.type = Unordered; + listItem.format = Bullet; + listStack.push(listItem); + } + } else if (char0 == QLatin1Char('h') && tagLength == 2) { + int level = tag.at(1).digitValue(); + if (level >= 1 && level <= 6) { + static const qreal scaling[] = { 2.0, 1.5, 1.2, 1.0, 0.8, 0.7 }; + if (!hasNewLine) + textOut.append(QChar::LineSeparator); + format.setFontPointSize(baseFont.pointSize() * scaling[level - 1]); + format.setFontWeight(QFont::Bold); + } + } else if (tag == QLatin1String("ol")) { + List listItem; + listItem.level = 0; + listItem.type = Ordered; + listItem.format = Decimal; + listStack.push(listItem); + } else if (tag == QLatin1String("li")) { + if (!hasNewLine) + textOut.append(QChar(QChar::LineSeparator)); + if (!listStack.isEmpty()) { + int count = ++listStack.top().level; + for (int i = 0; i < listStack.size(); ++i) + textOut += QString(tabsize, QChar::Nbsp); + switch (listStack.top().format) { + case Decimal: + textOut += QString::number(count) % QLatin1Char('.'); + break; + case LowerAlpha: + textOut += toAlpha(count, false) % QLatin1Char('.'); + break; + case UpperAlpha: + textOut += toAlpha(count, true) % QLatin1Char('.'); + break; + case LowerRoman: + textOut += toRoman(count, false) % QLatin1Char('.'); + break; + case UpperRoman: + textOut += toRoman(count, true) % QLatin1Char('.'); + break; + case Bullet: + textOut += bullet; + break; + case Disc: + textOut += disc; + break; + case Square: + textOut += square; + break; + } + textOut += QString(2, QChar::Nbsp); + } + } + return true; + } else if (ch->isSpace()) { + // may have params. + QStringRef tag(&textIn, tagStart, tagLength); + if (tag == QLatin1String("font")) + return parseFontAttributes(ch, textIn, format); + if (tag == QLatin1String("ol")) + return parseOrderedListAttributes(ch, textIn); + if (tag == QLatin1String("ul")) + return parseUnorderedListAttributes(ch, textIn); + if (tag == QLatin1String("a")) { + return parseAnchorAttributes(ch, textIn, format); + } + if (*ch == greaterThan || ch->isNull()) + continue; + } else if (*ch != slash) { + tagLength++; + } + ++ch; + } + return false; +} + +bool QDeclarativeStyledTextPrivate::parseCloseTag(const QChar *&ch, const QString &textIn, QString &textOut) +{ + skipSpace(ch); + + int tagStart = ch - textIn.constData(); + int tagLength = 0; + while (!ch->isNull()) { + if (*ch == greaterThan) { + if (tagLength == 0) + return false; + QStringRef tag(&textIn, tagStart, tagLength); + const QChar char0 = tag.at(0); + hasNewLine = false; + if (char0 == QLatin1Char('b')) { + if (tagLength == 1) + return true; + else if (tag.at(1) == QLatin1Char('r') && tagLength == 2) + return true; + } else if (char0 == QLatin1Char('i')) { + if (tagLength == 1) + return true; + } else if (char0 == QLatin1Char('a')) { + if (tagLength == 1) + return true; + } else if (char0 == QLatin1Char('p')) { + if (tagLength == 1) { + textOut.append(QChar::LineSeparator); + hasNewLine = true; + return true; + } + } else if (char0 == QLatin1Char('u')) { + if (tagLength == 1) + return true; + else if (tag == QLatin1String("ul")) { + if (!listStack.isEmpty()) { + listStack.pop(); + if (!listStack.count()) + textOut.append(QChar::LineSeparator); + } + return true; + } + } else if (char0 == QLatin1Char('h') && tagLength == 2) { + textOut.append(QChar::LineSeparator); + hasNewLine = true; + return true; + } else if (tag == QLatin1String("font")) { + return true; + } else if (tag == QLatin1String("ol")) { + if (!listStack.isEmpty()) { + listStack.pop(); + if (!listStack.count()) + textOut.append(QChar::LineSeparator); + } + return true; + } else if (tag == QLatin1String("li")) { + return true; + } + return false; + } else if (!ch->isSpace()){ + tagLength++; + } + ++ch; + } + + return false; +} + +void QDeclarativeStyledTextPrivate::parseEntity(const QChar *&ch, const QString &textIn, QString &textOut) +{ + int entityStart = ch - textIn.constData(); + int entityLength = 0; + while (!ch->isNull()) { + if (*ch == QLatin1Char(';')) { + QStringRef entity(&textIn, entityStart, entityLength); + if (entity == QLatin1String("gt")) + textOut += QChar(62); + else if (entity == QLatin1String("lt")) + textOut += QChar(60); + else if (entity == QLatin1String("amp")) + textOut += QChar(38); + return; + } + ++entityLength; + ++ch; + } +} + +bool QDeclarativeStyledTextPrivate::parseFontAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format) +{ + bool valid = false; + QPair<QStringRef,QStringRef> attr; + do { + attr = parseAttribute(ch, textIn); + if (attr.first == QLatin1String("color")) { + valid = true; + format.setForeground(QColor(attr.second.toString())); + } else if (attr.first == QLatin1String("size")) { + valid = true; + int size = attr.second.toString().toInt(); + if (attr.second.at(0) == QLatin1Char('-') || attr.second.at(0) == QLatin1Char('+')) + size += 3; + if (size >= 1 && size <= 7) { + static const qreal scaling[] = { 0.7, 0.8, 1.0, 1.2, 1.5, 2.0, 2.4 }; + format.setFontPointSize(baseFont.pointSize() * scaling[size-1]); + } + } + } while (!ch->isNull() && !attr.first.isEmpty()); + + return valid; +} + +bool QDeclarativeStyledTextPrivate::parseOrderedListAttributes(const QChar *&ch, const QString &textIn) +{ + bool valid = false; + + List listItem; + listItem.level = 0; + listItem.type = Ordered; + listItem.format = Decimal; + + QPair<QStringRef,QStringRef> attr; + do { + attr = parseAttribute(ch, textIn); + if (attr.first == QLatin1String("type")) { + valid = true; + if (attr.second == QLatin1String("a")) + listItem.format = LowerAlpha; + else if (attr.second == QLatin1String("A")) + listItem.format = UpperAlpha; + else if (attr.second == QLatin1String("i")) + listItem.format = LowerRoman; + else if (attr.second == QLatin1String("I")) + listItem.format = UpperRoman; + } + } while (!ch->isNull() && !attr.first.isEmpty()); + + listStack.push(listItem); + return valid; +} + +bool QDeclarativeStyledTextPrivate::parseUnorderedListAttributes(const QChar *&ch, const QString &textIn) +{ + bool valid = false; + + List listItem; + listItem.level = 0; + listItem.type = Unordered; + listItem.format = Bullet; + + QPair<QStringRef,QStringRef> attr; + do { + attr = parseAttribute(ch, textIn); + if (attr.first == QLatin1String("type")) { + valid = true; + if (attr.second == QLatin1String("disc")) + listItem.format = Disc; + else if (attr.second == QLatin1String("square")) + listItem.format = Square; + } + } while (!ch->isNull() && !attr.first.isEmpty()); + + listStack.push(listItem); + return valid; +} + +bool QDeclarativeStyledTextPrivate::parseAnchorAttributes(const QChar *&ch, const QString &textIn, QTextCharFormat &format) +{ + bool valid = false; + + QPair<QStringRef,QStringRef> attr; + do { + attr = parseAttribute(ch, textIn); + if (attr.first == QLatin1String("href")) { + format.setAnchorHref(attr.second.toString()); + format.setAnchor(true); + format.setFontUnderline(true); + format.setForeground(QColor("blue")); + valid = true; + } + } while (!ch->isNull() && !attr.first.isEmpty()); + + return valid; +} + +QPair<QStringRef,QStringRef> QDeclarativeStyledTextPrivate::parseAttribute(const QChar *&ch, const QString &textIn) +{ + skipSpace(ch); + + int attrStart = ch - textIn.constData(); + int attrLength = 0; + while (!ch->isNull()) { + if (*ch == greaterThan) { + break; + } else if (*ch == equals) { + ++ch; + if (*ch != singleQuote && *ch != doubleQuote) { + while (*ch != greaterThan && !ch->isNull()) + ++ch; + break; + } + ++ch; + if (!attrLength) + break; + QStringRef attr(&textIn, attrStart, attrLength); + QStringRef val = parseValue(ch, textIn); + if (!val.isEmpty()) + return QPair<QStringRef,QStringRef>(attr,val); + break; + } else { + ++attrLength; + } + ++ch; + } + + return QPair<QStringRef,QStringRef>(); +} + +QStringRef QDeclarativeStyledTextPrivate::parseValue(const QChar *&ch, const QString &textIn) +{ + int valStart = ch - textIn.constData(); + int valLength = 0; + while (*ch != singleQuote && *ch != doubleQuote && !ch->isNull()) { + ++valLength; + ++ch; + } + if (ch->isNull()) + return QStringRef(); + ++ch; // skip quote + + return QStringRef(&textIn, valStart, valLength); +} + +QString QDeclarativeStyledTextPrivate::toAlpha(int value, bool upper) +{ + const char baseChar = upper ? 'A' : 'a'; + + QString result; + int c = value; + while (c > 0) { + c--; + result.prepend(QChar(baseChar + (c % 26))); + c /= 26; + } + return result; +} + +QString QDeclarativeStyledTextPrivate::toRoman(int value, bool upper) +{ + QString result = QLatin1String("?"); + // works for up to 4999 items + if (value < 5000) { + QByteArray romanNumeral; + + static const char romanSymbolsLower[] = "iiivixxxlxcccdcmmmm"; + static const char romanSymbolsUpper[] = "IIIVIXXXLXCCCDCMMMM"; + QByteArray romanSymbols; + if (!upper) + romanSymbols = QByteArray::fromRawData(romanSymbolsLower, sizeof(romanSymbolsLower)); + else + romanSymbols = QByteArray::fromRawData(romanSymbolsUpper, sizeof(romanSymbolsUpper)); + + int c[] = { 1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000 }; + int n = value; + for (int i = 12; i >= 0; n %= c[i], i--) { + int q = n / c[i]; + if (q > 0) { + int startDigit = i + (i + 3) / 4; + int numDigits; + if (i % 4) { + if ((i - 2) % 4) + numDigits = 2; + else + numDigits = 1; + } + else + numDigits = q; + romanNumeral.append(romanSymbols.mid(startDigit, numDigits)); + } + } + result = QString::fromLatin1(romanNumeral); + } + return result; +} + +QT_END_NAMESPACE |