diff options
author | Lars Knoll <lars.knoll@nokia.com> | 2009-03-23 09:34:13 (GMT) |
---|---|---|
committer | Simon Hausmann <simon.hausmann@nokia.com> | 2009-03-23 09:34:13 (GMT) |
commit | 67ad0519fd165acee4a4d2a94fa502e9e4847bd0 (patch) | |
tree | 1dbf50b3dff8d5ca7e9344733968c72704eb15ff /tools/linguist/shared | |
download | Qt-67ad0519fd165acee4a4d2a94fa502e9e4847bd0.zip Qt-67ad0519fd165acee4a4d2a94fa502e9e4847bd0.tar.gz Qt-67ad0519fd165acee4a4d2a94fa502e9e4847bd0.tar.bz2 |
Long live Qt!
Diffstat (limited to 'tools/linguist/shared')
21 files changed, 8614 insertions, 0 deletions
diff --git a/tools/linguist/shared/abstractproitemvisitor.h b/tools/linguist/shared/abstractproitemvisitor.h new file mode 100644 index 0000000..b108e7e --- /dev/null +++ b/tools/linguist/shared/abstractproitemvisitor.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef ABSTRACTPROITEMVISITOR +#define ABSTRACTPROITEMVISITOR + +#include "proitems.h" + +QT_BEGIN_NAMESPACE + +struct AbstractProItemVisitor +{ + virtual ~AbstractProItemVisitor() {} + virtual bool visitBeginProBlock(ProBlock *block) = 0; + virtual bool visitEndProBlock(ProBlock *block) = 0; + + virtual bool visitBeginProVariable(ProVariable *variable) = 0; + virtual bool visitEndProVariable(ProVariable *variable) = 0; + + virtual bool visitBeginProFile(ProFile *value) = 0; + virtual bool visitEndProFile(ProFile *value) = 0; + + virtual bool visitProValue(ProValue *value) = 0; + virtual bool visitProFunction(ProFunction *function) = 0; + virtual bool visitProOperator(ProOperator *function) = 0; + virtual bool visitProCondition(ProCondition *function) = 0; +}; + +QT_END_NAMESPACE + +#endif // ABSTRACTPROITEMVISITOR + diff --git a/tools/linguist/shared/formats.pri b/tools/linguist/shared/formats.pri new file mode 100644 index 0000000..985f6db --- /dev/null +++ b/tools/linguist/shared/formats.pri @@ -0,0 +1,22 @@ + +# infrastructure +QT *= xml + +INCLUDEPATH *= $$PWD + +SOURCES += \ + $$PWD/numerus.cpp \ + $$PWD/translator.cpp \ + $$PWD/translatormessage.cpp + +HEADERS += \ + $$PWD/translator.h \ + $$PWD/translatormessage.h + +# "real" formats readers and writers +SOURCES += \ + $$PWD/qm.cpp \ + $$PWD/qph.cpp \ + $$PWD/po.cpp \ + $$PWD/ts.cpp \ + $$PWD/xliff.cpp diff --git a/tools/linguist/shared/numerus.cpp b/tools/linguist/shared/numerus.cpp new file mode 100644 index 0000000..f3a29cc --- /dev/null +++ b/tools/linguist/shared/numerus.cpp @@ -0,0 +1,377 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/QByteArray> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QMap> + +#include <private/qtranslator_p.h> + +QT_BEGIN_NAMESPACE + +static const uchar englishStyleRules[] = + { Q_EQ, 1 }; +static const uchar frenchStyleRules[] = + { Q_LEQ, 1 }; +static const uchar latvianRules[] = + { Q_MOD_10 | Q_EQ, 1, Q_AND, Q_MOD_100 | Q_NEQ, 11, Q_NEWRULE, + Q_NEQ, 0 }; +static const uchar irishStyleRules[] = + { Q_EQ, 1, Q_NEWRULE, + Q_EQ, 2 }; +static const uchar czechRules[] = + { Q_MOD_100 | Q_EQ, 1, Q_NEWRULE, + Q_MOD_100 | Q_BETWEEN, 2, 4 }; +static const uchar slovakRules[] = + { Q_EQ, 1, Q_NEWRULE, + Q_BETWEEN, 2, 4 }; +static const uchar macedonianRules[] = + { Q_MOD_10 | Q_EQ, 1, Q_NEWRULE, + Q_MOD_10 | Q_EQ, 2 }; +static const uchar lithuanianRules[] = + { Q_MOD_10 | Q_EQ, 1, Q_AND, Q_MOD_100 | Q_NEQ, 11, Q_NEWRULE, + Q_MOD_10 | Q_EQ, 2, Q_AND, Q_MOD_100 | Q_NOT_BETWEEN, 10, 19 }; +static const uchar russianStyleRules[] = + { Q_MOD_10 | Q_EQ, 1, Q_AND, Q_MOD_100 | Q_NEQ, 11, Q_NEWRULE, + Q_MOD_10 | Q_BETWEEN, 2, 4, Q_AND, Q_MOD_100 | Q_NOT_BETWEEN, 10, 19 }; +static const uchar polishRules[] = + { Q_EQ, 1, Q_NEWRULE, + Q_MOD_10 | Q_BETWEEN, 2, 4, Q_AND, Q_MOD_100 | Q_NOT_BETWEEN, 10, 19 }; +static const uchar romanianRules[] = + { Q_EQ, 1, Q_NEWRULE, + Q_EQ, 0, Q_OR, Q_MOD_100 | Q_BETWEEN, 1, 19 }; +static const uchar slovenianRules[] = + { Q_MOD_100 | Q_EQ, 1, Q_NEWRULE, + Q_MOD_100 | Q_EQ, 2, Q_NEWRULE, + Q_MOD_100 | Q_BETWEEN, 3, 4 }; +static const uchar malteseRules[] = + { Q_EQ, 1, Q_NEWRULE, + Q_EQ, 0, Q_OR, Q_MOD_100 | Q_BETWEEN, 1, 10, Q_NEWRULE, + Q_MOD_100 | Q_BETWEEN, 11, 19 }; +static const uchar welshRules[] = + { Q_EQ, 0, Q_NEWRULE, + Q_EQ, 1, Q_NEWRULE, + Q_BETWEEN, 2, 5, Q_NEWRULE, + Q_EQ, 6 }; +static const uchar arabicRules[] = + { Q_EQ, 0, Q_NEWRULE, + Q_EQ, 1, Q_NEWRULE, + Q_EQ, 2, Q_NEWRULE, + Q_MOD_100 | Q_BETWEEN, 3, 10, Q_NEWRULE, + Q_MOD_100 | Q_NEQ, 0 }; + +static const char * const japaneseStyleForms[] = { "Universal Form", 0 }; +static const char * const englishStyleForms[] = { "Singular", "Plural", 0 }; +static const char * const frenchStyleForms[] = { "Singular", "Plural", 0 }; +static const char * const latvianForms[] = { "Singular", "Plural", "Nullar", 0 }; +static const char * const irishStyleForms[] = { "Singular", "Dual", "Plural", 0 }; +static const char * const czechForms[] = { "Singular", "Dual", "Plural", 0 }; +static const char * const slovakForms[] = { "Singular", "Dual", "Plural", 0 }; +static const char * const macedonianForms[] = { "Singular", "Dual", "Plural", 0 }; +static const char * const lithuanianForms[] = { "Singular", "Dual", "Plural", 0 }; +static const char * const russianStyleForms[] = { "Singular", "Dual", "Plural", 0 }; +static const char * const polishForms[] = { "Singular", "Paucal", "Plural", 0 }; +static const char * const romanianForms[] = + { "Singular", "Plural Form for 2 to 19", "Plural", 0 }; +static const char * const slovenianForms[] = { "Singular", "Dual", "Trial", "Plural", 0 }; +static const char * const malteseForms[] = + { "Singular", "Plural Form for 2 to 10", "Plural Form for 11 to 19", "Plural", 0 }; +static const char * const welshForms[] = + { "Nullar", "Singular", "Dual", "Sexal", "Plural", 0 }; +static const char * const arabicForms[] = + { "Nullar", "Singular", "Dual", "Minority Plural", "Plural", "Plural Form for 100, 200, ...", 0 }; + +#define EOL QLocale::C + +static const QLocale::Language japaneseStyleLanguages[] = { + QLocale::Afan, + QLocale::Armenian, + QLocale::Bhutani, + QLocale::Bislama, + QLocale::Burmese, + QLocale::Chinese, + QLocale::FijiLanguage, + QLocale::Guarani, + QLocale::Hungarian, + QLocale::Indonesian, + QLocale::Japanese, + QLocale::Javanese, + QLocale::Korean, + QLocale::Malay, + QLocale::NauruLanguage, + QLocale::Persian, + QLocale::Sundanese, + QLocale::Thai, + QLocale::Tibetan, + QLocale::Vietnamese, + QLocale::Yoruba, + QLocale::Zhuang, + EOL +}; + +static const QLocale::Language englishStyleLanguages[] = { + QLocale::Abkhazian, + QLocale::Afar, + QLocale::Afrikaans, + QLocale::Albanian, + QLocale::Amharic, + QLocale::Assamese, + QLocale::Aymara, + QLocale::Azerbaijani, + QLocale::Bashkir, + QLocale::Basque, + QLocale::Bengali, + QLocale::Bihari, + // Missing: Bokmal, + QLocale::Bulgarian, + QLocale::Cambodian, + QLocale::Catalan, + QLocale::Cornish, + QLocale::Corsican, + QLocale::Danish, + QLocale::Dutch, + QLocale::English, + QLocale::Esperanto, + QLocale::Estonian, + QLocale::Faroese, + QLocale::Finnish, + // Missing: Friulian, + QLocale::Frisian, + QLocale::Galician, + QLocale::Georgian, + QLocale::German, + QLocale::Greek, + QLocale::Greenlandic, + QLocale::Gujarati, + QLocale::Hausa, + QLocale::Hebrew, + QLocale::Hindi, + QLocale::Icelandic, + QLocale::Interlingua, + QLocale::Interlingue, + QLocale::Italian, + QLocale::Kannada, + QLocale::Kashmiri, + QLocale::Kazakh, + QLocale::Kinyarwanda, + QLocale::Kirghiz, + QLocale::Kurdish, + QLocale::Kurundi, + QLocale::Laothian, + QLocale::Latin, + // Missing: Letzeburgesch, + QLocale::Lingala, + QLocale::Malagasy, + QLocale::Malayalam, + QLocale::Marathi, + QLocale::Mongolian, + // Missing: Nahuatl, + QLocale::Nepali, + // Missing: Northern Sotho, + QLocale::Norwegian, + QLocale::Nynorsk, + QLocale::Occitan, + QLocale::Oriya, + QLocale::Pashto, + QLocale::Portuguese, + QLocale::Punjabi, + QLocale::Quechua, + QLocale::RhaetoRomance, + QLocale::Sesotho, + QLocale::Setswana, + QLocale::Shona, + QLocale::Sindhi, + QLocale::Singhalese, + QLocale::Siswati, + QLocale::Somali, + QLocale::Spanish, + QLocale::Swahili, + QLocale::Swedish, + QLocale::Tagalog, + QLocale::Tajik, + QLocale::Tamil, + QLocale::Tatar, + QLocale::Telugu, + QLocale::TongaLanguage, + QLocale::Tsonga, + QLocale::Turkish, + QLocale::Turkmen, + QLocale::Twi, + QLocale::Uigur, + QLocale::Uzbek, + QLocale::Volapuk, + QLocale::Wolof, + QLocale::Xhosa, + QLocale::Yiddish, + QLocale::Zulu, + EOL +}; +static const QLocale::Language frenchStyleLanguages[] = { + // keep synchronized with frenchStyleCountries + QLocale::Breton, + QLocale::French, + QLocale::Portuguese, + // Missing: Filipino, + QLocale::Tigrinya, + // Missing: Walloon + EOL +}; +static const QLocale::Language latvianLanguage[] = { QLocale::Latvian, EOL }; +static const QLocale::Language irishStyleLanguages[] = { + QLocale::Divehi, + QLocale::Gaelic, + QLocale::Inuktitut, + QLocale::Inupiak, + QLocale::Irish, + QLocale::Manx, + QLocale::Maori, + // Missing: Sami, + QLocale::Samoan, + QLocale::Sanskrit, + EOL +}; +static const QLocale::Language czechLanguage[] = { QLocale::Czech, EOL }; +static const QLocale::Language slovakLanguage[] = { QLocale::Slovak, EOL }; +static const QLocale::Language macedonianLanguage[] = { QLocale::Macedonian, EOL }; +static const QLocale::Language lithuanianLanguage[] = { QLocale::Lithuanian, EOL }; +static const QLocale::Language russianStyleLanguages[] = { + QLocale::Bosnian, + QLocale::Byelorussian, + QLocale::Croatian, + QLocale::Russian, + QLocale::Serbian, + QLocale::SerboCroatian, + QLocale::Ukrainian, + EOL +}; +static const QLocale::Language polishLanguage[] = { QLocale::Polish, EOL }; +static const QLocale::Language romanianLanguages[] = { + QLocale::Moldavian, + QLocale::Romanian, + EOL +}; +static const QLocale::Language slovenianLanguage[] = { QLocale::Slovenian, EOL }; +static const QLocale::Language malteseLanguage[] = { QLocale::Maltese, EOL }; +static const QLocale::Language welshLanguage[] = { QLocale::Welsh, EOL }; +static const QLocale::Language arabicLanguage[] = { QLocale::Arabic, EOL }; + +static const QLocale::Country frenchStyleCountries[] = { + // keep synchronized with frenchStyleLanguages + QLocale::AnyCountry, + QLocale::AnyCountry, + QLocale::Brazil, + QLocale::AnyCountry +}; +struct NumerusTableEntry { + const uchar *rules; + int rulesSize; + const char * const *forms; + const QLocale::Language *languages; + const QLocale::Country *countries; +}; + +static const NumerusTableEntry numerusTable[] = { + { 0, 0, japaneseStyleForms, japaneseStyleLanguages, 0 }, + { englishStyleRules, sizeof(englishStyleRules), englishStyleForms, englishStyleLanguages, 0 }, + { frenchStyleRules, sizeof(frenchStyleRules), frenchStyleForms, frenchStyleLanguages, + frenchStyleCountries }, + { latvianRules, sizeof(latvianRules), latvianForms, latvianLanguage, 0 }, + { irishStyleRules, sizeof(irishStyleRules), irishStyleForms, irishStyleLanguages, 0 }, + { czechRules, sizeof(czechRules), czechForms, czechLanguage, 0 }, + { slovakRules, sizeof(slovakRules), slovakForms, slovakLanguage, 0 }, + { macedonianRules, sizeof(macedonianRules), macedonianForms, macedonianLanguage, 0 }, + { lithuanianRules, sizeof(lithuanianRules), lithuanianForms, lithuanianLanguage, 0 }, + { russianStyleRules, sizeof(russianStyleRules), russianStyleForms, russianStyleLanguages, 0 }, + { polishRules, sizeof(polishRules), polishForms, polishLanguage, 0 }, + { romanianRules, sizeof(romanianRules), romanianForms, romanianLanguages, 0 }, + { slovenianRules, sizeof(slovenianRules), slovenianForms, slovenianLanguage, 0 }, + { malteseRules, sizeof(malteseRules), malteseForms, malteseLanguage, 0 }, + { welshRules, sizeof(welshRules), welshForms, welshLanguage, 0 }, + { arabicRules, sizeof(arabicRules), arabicForms, arabicLanguage, 0 } +}; + +static const int NumerusTableSize = sizeof(numerusTable) / sizeof(numerusTable[0]); + +// magic number for the file +static const int MagicLength = 16; +static const uchar magic[MagicLength] = { + 0x3c, 0xb8, 0x64, 0x18, 0xca, 0xef, 0x9c, 0x95, + 0xcd, 0x21, 0x1c, 0xbf, 0x60, 0xa1, 0xbd, 0xdd +}; + +bool getNumerusInfo(QLocale::Language language, QLocale::Country country, + QByteArray *rules, QStringList *forms) +{ + while (true) { + for (int i = 0; i < NumerusTableSize; ++i) { + const NumerusTableEntry &entry = numerusTable[i]; + for (int j = 0; entry.languages[j] != EOL; ++j) { + if (entry.languages[j] == language + && ((!entry.countries && country == QLocale::AnyCountry) + || (entry.countries && entry.countries[j] == country))) { + if (rules) { + *rules = QByteArray::fromRawData(reinterpret_cast<const char *>(entry.rules), + entry.rulesSize); + } + if (forms) { + forms->clear(); + for (int k = 0; entry.forms[k]; ++k) + forms->append(QLatin1String(entry.forms[k])); + } + return true; + } + } + } + + if (country == QLocale::AnyCountry) + break; + country = QLocale::AnyCountry; + } + return false; +} + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/po.cpp b/tools/linguist/shared/po.cpp new file mode 100644 index 0000000..e9375e9 --- /dev/null +++ b/tools/linguist/shared/po.cpp @@ -0,0 +1,662 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include <QtCore/QDebug> +#include <QtCore/QIODevice> +#include <QtCore/QHash> +#include <QtCore/QString> +#include <QtCore/QTextStream> + +#include <ctype.h> + +#define MAGIC_OBSOLETE_REFERENCE "Obsolete_PO_entries" + +// Uncomment if you wish to hard wrap long lines in .po files. Note that this +// affects only msg strings, not comments. +//#define HARD_WRAP_LONG_WORDS + +QT_BEGIN_NAMESPACE + +static const int MAX_LEN = 79; + +static QString poEscapedString(const QString &prefix, const QString &keyword, + bool noWrap, const QString &ba) +{ + QStringList lines; + int off = 0; + QString res; + while (off < ba.length()) { + ushort c = ba[off++].unicode(); + switch (c) { + case '\n': + res += QLatin1String("\\n"); + lines.append(res); + res.clear(); + break; + case '\r': + res += QLatin1String("\\r"); + break; + case '\t': + res += QLatin1String("\\t"); + break; + case '\v': + res += QLatin1String("\\v"); + break; + case '\a': + res += QLatin1String("\\a"); + break; + case '\b': + res += QLatin1String("\\b"); + break; + case '\f': + res += QLatin1String("\\f"); + break; + case '"': + res += QLatin1String("\\\""); + break; + case '\\': + res += QLatin1String("\\\\"); + break; + default: + if (c < 32) { + res += QLatin1String("\\x"); + res += QString::number(c, 16); + if (off < ba.length() && isxdigit(ba[off].unicode())) + res += QLatin1String("\"\""); + } else { + res += QChar(c); + } + break; + } + } + if (!res.isEmpty()) + lines.append(res); + if (!lines.isEmpty()) { + if (!noWrap) { + if (lines.count() != 1 || + lines.first().length() > MAX_LEN - keyword.length() - prefix.length() - 3) + { + QStringList olines = lines; + lines = QStringList(QString()); + const int maxlen = MAX_LEN - prefix.length() - 2; + foreach (const QString &line, olines) { + int off = 0; + while (off + maxlen < line.length()) { + int idx = line.lastIndexOf(QLatin1Char(' '), off + maxlen - 1) + 1; + if (idx == off) { +#ifdef HARD_WRAP_LONG_WORDS + // This doesn't seem too nice, but who knows ... + idx = off + maxlen; +#else + idx = line.indexOf(QLatin1Char(' '), off + maxlen) + 1; + if (!idx) + break; +#endif + } + lines.append(line.mid(off, idx - off)); + off = idx; + } + lines.append(line.mid(off)); + } + } + } else if (lines.count() > 1) { + lines.prepend(QString()); + } + } + return prefix + keyword + QLatin1String(" \"") + + lines.join(QLatin1String("\"\n") + prefix + QLatin1Char('"')) + + QLatin1String("\"\n"); +} + +static QString poEscapedLines(const QString &prefix, bool addSpace, const QStringList &lines) +{ + QString out; + foreach (const QString &line, lines) { + out += prefix; + if (addSpace && !line.isEmpty()) + out += QLatin1Char(' ' ); + out += line; + out += QLatin1Char('\n'); + } + return out; +} + +static QString poEscapedLines(const QString &prefix, bool addSpace, const QString &in0) +{ + QString in = in0; + if (in.endsWith(QLatin1Char('\n'))) + in.chop(1); + return poEscapedLines(prefix, addSpace, in.split(QLatin1Char('\n'))); +} + +static QString poWrappedEscapedLines(const QString &prefix, bool addSpace, const QString &line) +{ + const int maxlen = MAX_LEN - prefix.length(); + QStringList lines; + int off = 0; + while (off + maxlen < line.length()) { + int idx = line.lastIndexOf(QLatin1Char(' '), off + maxlen - 1); + if (idx < off) { +#if 0 //def HARD_WRAP_LONG_WORDS + // This cannot work without messing up semantics, so do not even try. +#else + idx = line.indexOf(QLatin1Char(' '), off + maxlen); + if (idx < 0) + break; +#endif + } + lines.append(line.mid(off, idx - off)); + off = idx + 1; + } + lines.append(line.mid(off)); + return poEscapedLines(prefix, addSpace, lines); +} + +struct PoItem +{ +public: + PoItem() + : isPlural(false), isFuzzy(false) + {} + + +public: + QString id; + QString context; + QString tscomment; + QString oldTscomment; + QString lineNumber; + QString fileName; + QString references; + QString translatorComments; + QString automaticComments; + QString msgId; + QString oldMsgId; + QStringList msgStr; + bool isPlural; + bool isFuzzy; + QHash<QString, QString> extra; +}; + + +static bool isTranslationLine(const QString &line) +{ + return line.startsWith(QLatin1String("#~ msgstr")) + || line.startsWith(QLatin1String("msgstr")); +} + +static QString slurpEscapedString(const QStringList &lines, int & l, + int offset, const QString &prefix, ConversionData &cd) +{ + QString msg; + int stoff; + + for (; l < lines.size(); ++l) { + const QString &line = lines.at(l); + if (line.isEmpty() || !line.startsWith(prefix)) + break; + while (line[offset].isSpace()) // No length check, as string has no trailing spaces. + offset++; + if (line[offset].unicode() != '"') + break; + offset++; + forever { + if (offset == line.length()) + goto premature_eol; + ushort c = line[offset++].unicode(); + if (c == '"') { + if (offset == line.length()) + break; + while (line[offset].isSpace()) + offset++; + if (line[offset++].unicode() != '"') { + cd.appendError(QString::fromLatin1( + "PO parsing error: extra characters on line %1.") + .arg(l + 1)); + break; + } + continue; + } + if (c == '\\') { + if (offset == line.length()) + goto premature_eol; + c = line[offset++].unicode(); + switch (c) { + case 'r': + msg += QLatin1Char('\r'); // Maybe just throw it away? + break; + case 'n': + msg += QLatin1Char('\n'); + break; + case 't': + msg += QLatin1Char('\t'); + break; + case 'v': + msg += QLatin1Char('\v'); + break; + case 'a': + msg += QLatin1Char('\a'); + break; + case 'b': + msg += QLatin1Char('\b'); + break; + case 'f': + msg += QLatin1Char('\f'); + break; + case '"': + msg += QLatin1Char('"'); + break; + case '\\': + msg += QLatin1Char('\\'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + stoff = offset - 1; + while ((c = line[offset].unicode()) >= '0' && c <= '7') + if (++offset == line.length()) + goto premature_eol; + msg += QChar(line.mid(stoff, offset - stoff).toUInt(0, 8)); + break; + case 'x': + stoff = offset; + while (isxdigit(line[offset].unicode())) + if (++offset == line.length()) + goto premature_eol; + msg += QChar(line.mid(stoff, offset - stoff).toUInt(0, 16)); + break; + default: + cd.appendError(QString::fromLatin1( + "PO parsing error: invalid escape '\\%1' (line %2).") + .arg(QChar(c)).arg(l + 1)); + msg += QLatin1Char('\\'); + msg += QChar(c); + break; + } + } else { + msg += QChar(c); + } + } + offset = prefix.size(); + } + --l; + return msg; + +premature_eol: + cd.appendError(QString::fromLatin1( + "PO parsing error: premature end of line %1.").arg(l + 1)); + return QString(); +} + +static void slurpComment(QString &msg, const QStringList &lines, int & l) +{ + const QChar newline = QLatin1Char('\n'); + QString prefix = lines.at(l); + for (int i = 1; ; i++) { + if (prefix.at(i).unicode() != ' ') { + prefix.truncate(i); + break; + } + } + for (; l < lines.size(); ++l) { + const QString &line = lines.at(l); + if (line.startsWith(prefix)) + msg += line.mid(prefix.size()); + else if (line != QLatin1String("#")) + break; + msg += newline; + } + --l; +} + +bool loadPO(Translator &translator, QIODevice &dev, ConversionData &cd) +{ + const QChar quote = QLatin1Char('"'); + const QChar newline = QLatin1Char('\n'); + QTextStream in(&dev); + bool error = false; + + // format of a .po file entry: + // white-space + // # translator-comments + // #. automatic-comments + // #: reference... + // #, flag... + // #~ msgctxt, msgid*, msgstr - used for obsoleted messages + // #| msgctxt, msgid* previous untranslated-string - for fuzzy message + // msgctx string-context + // msgid untranslated-string + // -- For singular: + // msgstr translated-string + // -- For plural: + // msgid_plural untranslated-string-plural + // msgstr[0] translated-string + // ... + + // we need line based lookahead below. + QStringList lines; + while (!in.atEnd()) + lines.append(in.readLine().trimmed()); + lines.append(QString()); + + int l = 0; + PoItem item; + for (; l != lines.size(); ++l) { + QString line = lines.at(l); + if (line.isEmpty()) + continue; + if (isTranslationLine(line)) { + bool isObsolete = line.startsWith(QLatin1String("#~ msgstr")); + const QString prefix = QLatin1String(isObsolete ? "#~ " : ""); + while (true) { + int idx = line.indexOf(QLatin1Char(' '), prefix.length()); + item.msgStr.append(slurpEscapedString(lines, l, idx, prefix, cd)); + if (l + 1 >= lines.size() || !isTranslationLine(lines.at(l + 1))) + break; + ++l; + line = lines.at(l); + } + if (item.msgId.isEmpty()) { + QRegExp rx(QLatin1String("\\bX-Language: ([^\n]*)\n")); + int idx = rx.indexIn(item.msgStr.first()); + if (idx >= 0) { + translator.setLanguageCode(rx.cap(1)); + item.msgStr.first().remove(idx, rx.matchedLength()); + } + QRegExp rx2(QLatin1String("\\bX-Source-Language: ([^\n]*)\n")); + int idx2 = rx2.indexIn(item.msgStr.first()); + if (idx2 >= 0) { + translator.setSourceLanguageCode(rx2.cap(1)); + item.msgStr.first().remove(idx2, rx2.matchedLength()); + } + if (item.msgStr.first().indexOf( + QRegExp(QLatin1String("\\bX-Virgin-Header:[^\n]*\n"))) >= 0) { + item = PoItem(); + continue; + } + } + // build translator message + TranslatorMessage msg; + msg.setContext(item.context); + if (!item.references.isEmpty()) { + foreach (const QString &ref, + item.references.split(QRegExp(QLatin1String("\\s")), + QString::SkipEmptyParts)) { + int pos = ref.lastIndexOf(QLatin1Char(':')); + if (pos != -1) + msg.addReference(ref.left(pos), ref.mid(pos + 1).toInt()); + } + } else if (isObsolete) { + msg.setFileName(QLatin1String(MAGIC_OBSOLETE_REFERENCE)); + } + msg.setId(item.id); + msg.setSourceText(item.msgId); + msg.setOldSourceText(item.oldMsgId); + msg.setComment(item.tscomment); + msg.setOldComment(item.oldTscomment); + msg.setExtraComment(item.automaticComments); + msg.setTranslatorComment(item.translatorComments); + msg.setPlural(item.isPlural || item.msgStr.size() > 1); + msg.setTranslations(item.msgStr); + if (isObsolete) + msg.setType(TranslatorMessage::Obsolete); + else if (item.isFuzzy) + msg.setType(TranslatorMessage::Unfinished); + else + msg.setType(TranslatorMessage::Finished); + msg.setExtras(item.extra); + + //qDebug() << "WRITE: " << context; + //qDebug() << "SOURCE: " << msg.sourceText(); + //qDebug() << flags << msg.m_extra; + translator.append(msg); + item = PoItem(); + } else if (line.startsWith(QLatin1Char('#'))) { + switch(line.size() < 2 ? 0 : line.at(1).unicode()) { + case ':': + item.references += line.mid(3); + item.references += newline; + break; + case ',': { + QStringList flags = + line.mid(2).split(QRegExp(QLatin1String("[, ]")), + QString::SkipEmptyParts); + if (flags.removeOne(QLatin1String("fuzzy"))) + item.isFuzzy = true; + TranslatorMessage::ExtraData::const_iterator it = + item.extra.find(QLatin1String("po-flags")); + if (it != item.extra.end()) + flags.prepend(*it); + if (!flags.isEmpty()) + item.extra[QLatin1String("po-flags")] = flags.join(QLatin1String(", ")); + break; + } + case 0: + item.translatorComments += newline; + break; + case ' ': + slurpComment(item.translatorComments, lines, l); + break; + case '.': + if (line.startsWith(QLatin1String("#. ts-context "))) { + item.context = line.mid(14); + } else if (line.startsWith(QLatin1String("#. ts-id "))) { + item.id = line.mid(9); + } else { + item.automaticComments += line.mid(3); + item.automaticComments += newline; + } + break; + case '|': + if (line.startsWith(QLatin1String("#| msgid "))) { + item.oldMsgId = slurpEscapedString(lines, l, 9, QLatin1String("#| "), cd); + } else if (line.startsWith(QLatin1String("#| msgid_plural "))) { + QString extra = slurpEscapedString(lines, l, 16, QLatin1String("#| "), cd); + if (extra != item.oldMsgId) + item.extra[QLatin1String("po-old_msgid_plural")] = extra; + } else if (line.startsWith(QLatin1String("#| msgctxt "))) { + item.oldTscomment = slurpEscapedString(lines, l, 11, QLatin1String("#| "), cd); + } else { + cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'\n")) + .arg(l + 1).arg(lines[l])); + error = true; + } + break; + case '~': + if (line.startsWith(QLatin1String("#~ msgid "))) { + item.msgId = slurpEscapedString(lines, l, 9, QLatin1String("#~ "), cd); + } else if (line.startsWith(QLatin1String("#~ msgid_plural "))) { + QString extra = slurpEscapedString(lines, l, 16, QLatin1String("#~ "), cd); + if (extra != item.msgId) + item.extra[QLatin1String("po-msgid_plural")] = extra; + item.isPlural = true; + } else if (line.startsWith(QLatin1String("#~ msgctxt "))) { + item.tscomment = slurpEscapedString(lines, l, 11, QLatin1String("#~ "), cd); + } else { + cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'\n")) + .arg(l + 1).arg(lines[l])); + error = true; + } + break; + default: + cd.appendError(QString(QLatin1String("PO-format parse error in line %1: '%2'\n")) + .arg(l + 1).arg(lines[l])); + error = true; + break; + } + } else if (line.startsWith(QLatin1String("msgctxt "))) { + item.tscomment = slurpEscapedString(lines, l, 8, QString(), cd); + } else if (line.startsWith(QLatin1String("msgid "))) { + item.msgId = slurpEscapedString(lines, l, 6, QString(), cd); + } else if (line.startsWith(QLatin1String("msgid_plural "))) { + QString extra = slurpEscapedString(lines, l, 13, QString(), cd); + if (extra != item.msgId) + item.extra[QLatin1String("po-msgid_plural")] = extra; + item.isPlural = true; + } else { + cd.appendError(QString(QLatin1String("PO-format error in line %1: '%2'\n")) + .arg(l + 1).arg(lines[l])); + error = true; + } + } + return !error && cd.errors().isEmpty(); +} + +bool savePO(const Translator &translator, QIODevice &dev, ConversionData &cd) +{ + bool ok = true; + QTextStream out(&dev); + //qDebug() << "OUT CODEC: " << out.codec()->name(); + + bool first = true; + if (translator.messages().isEmpty() || !translator.messages().first().sourceText().isEmpty()) { + out << + "# SOME DESCRIPTIVE TITLE.\n" + "# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n" + "# This file is distributed under the same license as the PACKAGE package.\n" + "# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.\n" + "#\n" + "#, fuzzy\n" + "msgid \"\"\n" + "msgstr \"\"\n" + "\"X-Virgin-Header: remove this line if you change anything in the header.\\n\"\n"; + if (!translator.languageCode().isEmpty()) + out << "\"X-Language: " << translator.languageCode() << "\\n\"\n"; + if (!translator.sourceLanguageCode().isEmpty()) + out << "\"X-Source-Language: " << translator.sourceLanguageCode() << "\\n\"\n"; + first = false; + } + foreach (const TranslatorMessage &msg, translator.messages()) { + if (!first) + out << endl; + + if (!msg.translatorComment().isEmpty()) + out << poEscapedLines(QLatin1String("#"), true, msg.translatorComment()); + + if (!msg.extraComment().isEmpty()) + out << poEscapedLines(QLatin1String("#."), true, msg.extraComment()); + + if (!msg.context().isEmpty()) + out << QLatin1String("#. ts-context ") << msg.context() << '\n'; + if (!msg.id().isEmpty()) + out << QLatin1String("#. ts-id ") << msg.id() << '\n'; + + if (!msg.fileName().isEmpty() && msg.fileName() != QLatin1String(MAGIC_OBSOLETE_REFERENCE)) { + QStringList refs; + foreach (const TranslatorMessage::Reference &ref, msg.allReferences()) + refs.append(QString(QLatin1String("%2:%1")) + .arg(ref.lineNumber()).arg(ref.fileName())); + out << poWrappedEscapedLines(QLatin1String("#:"), true, refs.join(QLatin1String(" "))); + } + + bool noWrap = false; + QStringList flags; + if (msg.type() == TranslatorMessage::Unfinished) + flags.append(QLatin1String("fuzzy")); + TranslatorMessage::ExtraData::const_iterator itr = + msg.extras().find(QLatin1String("po-flags")); + if (itr != msg.extras().end()) { + if (itr->split(QLatin1String(", ")).contains(QLatin1String("no-wrap"))) + noWrap = true; + flags.append(*itr); + } + if (!flags.isEmpty()) + out << "#, " << flags.join(QLatin1String(", ")) << '\n'; + + QString prefix = QLatin1String("#| "); + if (!msg.oldComment().isEmpty()) + out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap, msg.oldComment()); + if (!msg.oldSourceText().isEmpty()) + out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.oldSourceText()); + QString plural = msg.extra(QLatin1String("po-old_msgid_plural")); + if (!plural.isEmpty()) + out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural); + prefix = QLatin1String((msg.type() == TranslatorMessage::Obsolete) ? "#~ " : ""); + if (!msg.comment().isEmpty()) + out << poEscapedString(prefix, QLatin1String("msgctxt"), noWrap, msg.comment()); + out << poEscapedString(prefix, QLatin1String("msgid"), noWrap, msg.sourceText()); + if (!msg.isPlural()) { + QString transl = msg.translation(); + if (first) { + transl.remove(QRegExp(QLatin1String("\\bX-Language:[^\n]*\n"))); + if (!translator.languageCode().isEmpty()) + transl += QLatin1String("X-Language: ") + translator.languageCode() + QLatin1Char('\n'); + } + out << poEscapedString(prefix, QLatin1String("msgstr"), noWrap, transl); + } else { + QString plural = msg.extra(QLatin1String("po-msgid_plural")); + if (plural.isEmpty()) + plural = msg.sourceText(); + out << poEscapedString(prefix, QLatin1String("msgid_plural"), noWrap, plural); + QStringList translations = translator.normalizedTranslations(msg, cd, &ok); + for (int i = 0; i != translations.size(); ++i) { + out << poEscapedString(prefix, QString::fromLatin1("msgstr[%1]").arg(i), noWrap, + translations.at(i)); + } + } + first = false; + } + return ok; +} + +int initPO() +{ + Translator::FileFormat format; + format.extension = QLatin1String("po"); + format.description = QObject::tr("GNU Gettext localization files"); + format.loader = &loadPO; + format.saver = &savePO; + format.fileType = Translator::FileFormat::TranslationSource; + format.priority = 1; + Translator::registerFileFormat(format); + return 1; +} + +Q_CONSTRUCTOR_FUNCTION(initPO) + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/profileevaluator.cpp b/tools/linguist/shared/profileevaluator.cpp new file mode 100644 index 0000000..b9984cc --- /dev/null +++ b/tools/linguist/shared/profileevaluator.cpp @@ -0,0 +1,2357 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "profileevaluator.h" +#include "proparserutils.h" +#include "proitems.h" + +#include <QtCore/QByteArray> +#include <QtCore/QDateTime> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QList> +#include <QtCore/QRegExp> +#include <QtCore/QSet> +#include <QtCore/QStack> +#include <QtCore/QString> +#include <QtCore/QStringList> +#include <QtCore/QTextStream> + +#ifdef Q_OS_UNIX +#include <unistd.h> +#include <sys/utsname.h> +#elif defined(Q_OS_WIN32) +#include <Windows.h> +#endif +#include <stdio.h> +#include <stdlib.h> + +#ifdef Q_OS_WIN32 +#define QT_POPEN _popen +#else +#define QT_POPEN popen +#endif + +QT_BEGIN_NAMESPACE + +/////////////////////////////////////////////////////////////////////// +// +// Option +// +/////////////////////////////////////////////////////////////////////// + +QString +Option::fixString(QString string, uchar flags) +{ + // XXX Ripped out caching, so this will be slow. Should not matter for current uses. + + //fix the environment variables + if (flags & Option::FixEnvVars) { + int rep; + QRegExp reg_variableName(QLatin1String("\\$\\(.*\\)")); + reg_variableName.setMinimal(true); + while ((rep = reg_variableName.indexIn(string)) != -1) + string.replace(rep, reg_variableName.matchedLength(), + QString::fromLocal8Bit(qgetenv(string.mid(rep + 2, reg_variableName.matchedLength() - 3).toLatin1().constData()).constData())); + } + + //canonicalize it (and treat as a path) + if (flags & Option::FixPathCanonicalize) { +#if 0 + string = QFileInfo(string).canonicalFilePath(); +#endif + string = QDir::cleanPath(string); + } + + if (string.length() > 2 && string[0].isLetter() && string[1] == QLatin1Char(':')) + string[0] = string[0].toLower(); + + //fix separators + Q_ASSERT(!((flags & Option::FixPathToLocalSeparators) && (flags & Option::FixPathToTargetSeparators))); + if (flags & Option::FixPathToLocalSeparators) { +#if defined(Q_OS_WIN32) + string = string.replace(QLatin1Char('/'), QLatin1Char('\\')); +#else + string = string.replace(QLatin1Char('\\'), QLatin1Char('/')); +#endif + } else if (flags & Option::FixPathToTargetSeparators) { + string = string.replace(QLatin1Char('/'), Option::dir_sep) + .replace(QLatin1Char('\\'), Option::dir_sep); + } + + if ((string.startsWith(QLatin1Char('"')) && string.endsWith(QLatin1Char('"'))) || + (string.startsWith(QLatin1Char('\'')) && string.endsWith(QLatin1Char('\'')))) + string = string.mid(1, string.length() - 2); + + return string; +} + +/////////////////////////////////////////////////////////////////////// +// +// ProFileEvaluator::Private +// +/////////////////////////////////////////////////////////////////////// + +class ProFileEvaluator::Private : public AbstractProItemVisitor +{ +public: + Private(ProFileEvaluator *q_); + + ProFileEvaluator *q; + int m_lineNo; // Error reporting + bool m_verbose; + + /////////////// Reading pro file + + bool read(ProFile *pro); + + ProBlock *currentBlock(); + void updateItem(); + bool parseLine(const QString &line); + void insertVariable(const QString &line, int *i); + void insertOperator(const char op); + void insertComment(const QString &comment); + void enterScope(bool multiLine); + void leaveScope(); + void finalizeBlock(); + + QStack<ProBlock *> m_blockstack; + ProBlock *m_block; + + ProItem *m_commentItem; + QString m_proitem; + QString m_pendingComment; + bool m_syntaxError; + bool m_contNextLine; + + /////////////// Evaluating pro file contents + + // implementation of AbstractProItemVisitor + bool visitBeginProBlock(ProBlock *block); + bool visitEndProBlock(ProBlock *block); + bool visitBeginProVariable(ProVariable *variable); + bool visitEndProVariable(ProVariable *variable); + bool visitBeginProFile(ProFile *value); + bool visitEndProFile(ProFile *value); + bool visitProValue(ProValue *value); + bool visitProFunction(ProFunction *function); + bool visitProOperator(ProOperator *oper); + bool visitProCondition(ProCondition *condition); + + QStringList valuesDirect(const QString &variableName) const { return m_valuemap[variableName]; } + QStringList values(const QString &variableName) const; + QStringList values(const QString &variableName, const ProFile *pro) const; + QStringList values(const QString &variableName, const QHash<QString, QStringList> &place, + const ProFile *pro) const; + QString propertyValue(const QString &val) const; + + bool isActiveConfig(const QString &config, bool regex = false); + QStringList expandPattern(const QString &pattern); + void expandPatternHelper(const QString &relName, const QString &absName, + QStringList &sources_out); + QStringList expandVariableReferences(const QString &value); + QStringList evaluateExpandFunction(const QString &function, const QString &arguments); + QString format(const char *format) const; + + QString currentFileName() const; + QString currentDirectory() const; + ProFile *currentProFile() const; + + bool evaluateConditionalFunction(const QString &function, const QString &arguments, bool *result); + bool evaluateFile(const QString &fileName, bool *result); + bool evaluateFeatureFile(const QString &fileName, bool *result); + + QStringList qmakeFeaturePaths(); + + enum { ConditionTrue, ConditionFalse, ConditionElse }; + int m_condition; + int m_prevCondition; + bool m_updateCondition; + bool m_invertNext; + int m_skipLevel; + bool m_cumulative; + bool m_isFirstVariableValue; + QString m_lastVarName; + ProVariable::VariableOperator m_variableOperator; + QString m_origfile; + QString m_oldPath; // To restore the current path to the path + QStack<ProFile*> m_profileStack; // To handle 'include(a.pri), so we can track back to 'a.pro' when finished with 'a.pri' + + // we need the following two variables for handling + // CONFIG = foo bar $$CONFIG + QHash<QString, QStringList> m_tempValuemap; // used while evaluating (variable operator value1 value2 ...) + QHash<const ProFile*, QHash<QString, QStringList> > m_tempFilevaluemap; // used while evaluating (variable operator value1 value2 ...) + + QHash<QString, QStringList> m_valuemap; // VariableName must be us-ascii, the content however can be non-us-ascii. + QHash<const ProFile*, QHash<QString, QStringList> > m_filevaluemap; // Variables per include file + QHash<QString, QString> m_properties; + QString m_outputDir; + + int m_prevLineNo; // Checking whether we're assigning the same TARGET + ProFile *m_prevProFile; // See m_prevLineNo +}; + +ProFileEvaluator::Private::Private(ProFileEvaluator *q_) + : q(q_) +{ + m_prevLineNo = 0; + m_prevProFile = 0; + m_verbose = true; + m_block = 0; + m_commentItem = 0; + m_syntaxError = 0; + m_lineNo = 0; + m_contNextLine = false; + m_cumulative = true; + m_updateCondition = false; + m_condition = ConditionFalse; + m_invertNext = false; + m_skipLevel = 0; + m_isFirstVariableValue = true; +} + +bool ProFileEvaluator::Private::read(ProFile *pro) +{ + QFile file(pro->fileName()); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + q->errorMessage(format("%1 not readable.").arg(pro->fileName())); + return false; + } + + m_syntaxError = false; + m_lineNo = 1; + m_blockstack.push(pro); + + QTextStream ts(&file); + while (!ts.atEnd()) { + QString line = ts.readLine(); + if (!parseLine(line)) { + q->errorMessage(format(".pro parse failure.")); + return false; + } + ++m_lineNo; + } + return true; +} + +bool ProFileEvaluator::Private::parseLine(const QString &line0) +{ + if (m_blockstack.isEmpty()) + return false; + + ushort quote = 0; + int parens = 0; + bool contNextLine = false; + QString line = line0.simplified(); + + for (int i = 0; !m_syntaxError && i < line.length(); ++i) { + ushort c = line.at(i).unicode(); + if (quote && c == quote) + quote = 0; + else if (c == '(') + ++parens; + else if (c == ')') + --parens; + else if (c == '"' && (i == 0 || line.at(i - 1).unicode() != '\\')) + quote = c; + else if (!parens && !quote) { + if (c == '#') { + insertComment(line.mid(i + 1)); + contNextLine = m_contNextLine; + break; + } + if (c == '\\' && i >= line.count() - 1) { + updateItem(); + contNextLine = true; + continue; + } + if (m_block && (m_block->blockKind() & ProBlock::VariableKind)) { + if (c == ' ') + updateItem(); + else + m_proitem += c; + continue; + } + if (c == ':') { + enterScope(false); + continue; + } + if (c == '{') { + enterScope(true); + continue; + } + if (c == '}') { + leaveScope(); + continue; + } + if (c == '=') { + insertVariable(line, &i); + continue; + } + if (c == '|' || c == '!') { + insertOperator(c); + continue; + } + } + + m_proitem += c; + } + m_contNextLine = contNextLine; + + if (!m_syntaxError) { + updateItem(); + if (!m_contNextLine) + finalizeBlock(); + } + return !m_syntaxError; +} + +void ProFileEvaluator::Private::finalizeBlock() +{ + if (m_blockstack.isEmpty()) { + m_syntaxError = true; + } else { + if (m_blockstack.top()->blockKind() & ProBlock::SingleLine) + leaveScope(); + m_block = 0; + m_commentItem = 0; + } +} + +void ProFileEvaluator::Private::insertVariable(const QString &line, int *i) +{ + ProVariable::VariableOperator opkind; + + switch (m_proitem.at(m_proitem.length() - 1).unicode()) { + case '+': + m_proitem.chop(1); + opkind = ProVariable::AddOperator; + break; + case '-': + m_proitem.chop(1); + opkind = ProVariable::RemoveOperator; + break; + case '*': + m_proitem.chop(1); + opkind = ProVariable::UniqueAddOperator; + break; + case '~': + m_proitem.chop(1); + opkind = ProVariable::ReplaceOperator; + break; + default: + opkind = ProVariable::SetOperator; + } + + ProBlock *block = m_blockstack.top(); + m_proitem = m_proitem.trimmed(); + ProVariable *variable = new ProVariable(m_proitem, block); + variable->setLineNumber(m_lineNo); + variable->setVariableOperator(opkind); + block->appendItem(variable); + m_block = variable; + + if (!m_pendingComment.isEmpty()) { + m_block->setComment(m_pendingComment); + m_pendingComment.clear(); + } + m_commentItem = variable; + + m_proitem.clear(); + + if (opkind == ProVariable::ReplaceOperator) { + // skip util end of line or comment + while (1) { + ++(*i); + + // end of line? + if (*i >= line.count()) + break; + + // comment? + if (line.at(*i).unicode() == '#') { + --(*i); + break; + } + + m_proitem += line.at(*i); + } + m_proitem = m_proitem.trimmed(); + } +} + +void ProFileEvaluator::Private::insertOperator(const char op) +{ + updateItem(); + + ProOperator::OperatorKind opkind; + switch(op) { + case '!': + opkind = ProOperator::NotOperator; + break; + case '|': + opkind = ProOperator::OrOperator; + break; + default: + opkind = ProOperator::OrOperator; + } + + ProBlock * const block = currentBlock(); + ProOperator * const proOp = new ProOperator(opkind); + proOp->setLineNumber(m_lineNo); + block->appendItem(proOp); + m_commentItem = proOp; +} + +void ProFileEvaluator::Private::insertComment(const QString &comment) +{ + updateItem(); + + QString strComment; + if (!m_commentItem) + strComment = m_pendingComment; + else + strComment = m_commentItem->comment(); + + if (strComment.isEmpty()) + strComment = comment; + else { + strComment += QLatin1Char('\n'); + strComment += comment.trimmed(); + } + + strComment = strComment.trimmed(); + + if (!m_commentItem) + m_pendingComment = strComment; + else + m_commentItem->setComment(strComment); +} + +void ProFileEvaluator::Private::enterScope(bool multiLine) +{ + updateItem(); + + ProBlock *parent = currentBlock(); + ProBlock *block = new ProBlock(parent); + block->setLineNumber(m_lineNo); + parent->setBlockKind(ProBlock::ScopeKind); + + parent->appendItem(block); + + if (multiLine) + block->setBlockKind(ProBlock::ScopeContentsKind); + else + block->setBlockKind(ProBlock::ScopeContentsKind|ProBlock::SingleLine); + + m_blockstack.push(block); + m_block = 0; +} + +void ProFileEvaluator::Private::leaveScope() +{ + updateItem(); + m_blockstack.pop(); + finalizeBlock(); +} + +ProBlock *ProFileEvaluator::Private::currentBlock() +{ + if (m_block) + return m_block; + + ProBlock *parent = m_blockstack.top(); + m_block = new ProBlock(parent); + m_block->setLineNumber(m_lineNo); + parent->appendItem(m_block); + + if (!m_pendingComment.isEmpty()) { + m_block->setComment(m_pendingComment); + m_pendingComment.clear(); + } + + m_commentItem = m_block; + + return m_block; +} + +void ProFileEvaluator::Private::updateItem() +{ + m_proitem = m_proitem.trimmed(); + if (m_proitem.isEmpty()) + return; + + ProBlock *block = currentBlock(); + if (block->blockKind() & ProBlock::VariableKind) { + m_commentItem = new ProValue(m_proitem, static_cast<ProVariable*>(block)); + } else if (m_proitem.endsWith(QLatin1Char(')'))) { + m_commentItem = new ProFunction(m_proitem); + } else { + m_commentItem = new ProCondition(m_proitem); + } + m_commentItem->setLineNumber(m_lineNo); + block->appendItem(m_commentItem); + + m_proitem.clear(); +} + + +bool ProFileEvaluator::Private::visitBeginProBlock(ProBlock *block) +{ + if (block->blockKind() == ProBlock::ScopeKind) { + m_updateCondition = true; + if (!m_skipLevel) { + m_prevCondition = m_condition; + m_condition = ConditionFalse; + } else { + Q_ASSERT(m_condition != ConditionTrue); + } + } else if (block->blockKind() & ProBlock::ScopeContentsKind) { + m_updateCondition = false; + if (m_condition != ConditionTrue) + ++m_skipLevel; + else + Q_ASSERT(!m_skipLevel); + } + return true; +} + +bool ProFileEvaluator::Private::visitEndProBlock(ProBlock *block) +{ + if (block->blockKind() & ProBlock::ScopeContentsKind) { + if (m_skipLevel) { + Q_ASSERT(m_condition != ConditionTrue); + --m_skipLevel; + } else { + // Conditionals contained inside this block may have changed the state. + // So we reset it here to make an else following us do the right thing. + m_condition = ConditionTrue; + } + } + return true; +} + +bool ProFileEvaluator::Private::visitBeginProVariable(ProVariable *variable) +{ + m_lastVarName = variable->variable(); + m_variableOperator = variable->variableOperator(); + m_isFirstVariableValue = true; + m_tempValuemap = m_valuemap; + m_tempFilevaluemap = m_filevaluemap; + return true; +} + +bool ProFileEvaluator::Private::visitEndProVariable(ProVariable *variable) +{ + Q_UNUSED(variable); + m_valuemap = m_tempValuemap; + m_filevaluemap = m_tempFilevaluemap; + m_lastVarName.clear(); + return true; +} + +bool ProFileEvaluator::Private::visitProOperator(ProOperator *oper) +{ + m_invertNext = (oper->operatorKind() == ProOperator::NotOperator); + return true; +} + +bool ProFileEvaluator::Private::visitProCondition(ProCondition *cond) +{ + if (!m_skipLevel) { + if (cond->text().toLower() == QLatin1String("else")) { + // The state ConditionElse makes sure that subsequential elses are ignored. + // That's braindead, but qmake is like that. + if (m_prevCondition == ConditionTrue) + m_condition = ConditionElse; + else if (m_prevCondition == ConditionFalse) + m_condition = ConditionTrue; + } else if (m_condition == ConditionFalse) { + if (isActiveConfig(cond->text(), true) ^ m_invertNext) + m_condition = ConditionTrue; + } + } + m_invertNext = false; + return true; +} + +bool ProFileEvaluator::Private::visitBeginProFile(ProFile * pro) +{ + PRE(pro); + bool ok = true; + m_lineNo = pro->lineNumber(); + + if (m_origfile.isEmpty()) + m_origfile = pro->fileName(); + if (m_oldPath.isEmpty()) { + // change the working directory for the initial profile we visit, since + // that is *the* profile. All the other times we reach this function will be due to + // include(file) or load(file) + + m_oldPath = QDir::currentPath(); + + m_profileStack.push(pro); + + const QString mkspecDirectory = propertyValue(QLatin1String("QMAKE_MKSPECS")); + if (!mkspecDirectory.isEmpty()) { + bool cumulative = m_cumulative; + m_cumulative = false; + // This is what qmake does, everything set in the mkspec is also set + // But this also creates a lot of problems + evaluateFile(mkspecDirectory + QLatin1String("/default/qmake.conf"), &ok); + evaluateFile(mkspecDirectory + QLatin1String("/features/default_pre.prf"), &ok); + m_cumulative = cumulative; + } + + ok = QDir::setCurrent(pro->directoryName()); + } + + return ok; +} + +bool ProFileEvaluator::Private::visitEndProFile(ProFile * pro) +{ + PRE(pro); + bool ok = true; + m_lineNo = pro->lineNumber(); + if (m_profileStack.count() == 1 && !m_oldPath.isEmpty()) { + const QString &mkspecDirectory = propertyValue(QLatin1String("QMAKE_MKSPECS")); + if (!mkspecDirectory.isEmpty()) { + bool cumulative = m_cumulative; + m_cumulative = false; + + evaluateFile(mkspecDirectory + QLatin1String("/features/default_post.prf"), &ok); + + QSet<QString> processed; + forever { + bool finished = true; + QStringList configs = valuesDirect(QLatin1String("CONFIG")); + for (int i = configs.size() - 1; i >= 0; --i) { + const QString config = configs[i].toLower(); + if (!processed.contains(config)) { + processed.insert(config); + evaluateFile(mkspecDirectory + QLatin1String("/features/") + + config + QLatin1String(".prf"), &ok); + if (ok) { + finished = false; + break; + } + } + } + if (finished) + break; + } + + m_cumulative = cumulative; + } + + m_profileStack.pop(); + ok = QDir::setCurrent(m_oldPath); + } + return ok; +} + +static void replaceInList(QStringList *varlist, + const QRegExp ®exp, const QString &replace, bool global) +{ + for (QStringList::Iterator varit = varlist->begin(); varit != varlist->end(); ) { + if ((*varit).contains(regexp)) { + (*varit).replace(regexp, replace); + if ((*varit).isEmpty()) + varit = varlist->erase(varit); + else + ++varit; + if(!global) + break; + } else { + ++varit; + } + } +} + +bool ProFileEvaluator::Private::visitProValue(ProValue *value) +{ + PRE(value); + m_lineNo = value->lineNumber(); + QString val = value->value(); + + QString varName = m_lastVarName; + + QStringList v = expandVariableReferences(val); + + // Since qmake combines different values for the TARGET variable, we join + // TARGET values that are on the same line. We can't do this later with all + // values because this parser isn't scope-aware, so we'd risk joining + // scope-specific targets together. + if (varName == QLatin1String("TARGET") + && m_lineNo == m_prevLineNo + && currentProFile() == m_prevProFile) { + QStringList targets = m_tempValuemap.value(QLatin1String("TARGET")); + m_tempValuemap.remove(QLatin1String("TARGET")); + QStringList lastTarget(targets.takeLast()); + lastTarget << v.join(QLatin1String(" ")); + targets.push_back(lastTarget.join(QLatin1String(" "))); + v = targets; + } + m_prevLineNo = m_lineNo; + m_prevProFile = currentProFile(); + + // The following two blocks fix bug 180128 by making all "interesting" + // file name absolute in each .pro file, not just the top most one + if (varName == QLatin1String("SOURCES") + || varName == QLatin1String("HEADERS") + || varName == QLatin1String("INTERFACES") + || varName == QLatin1String("FORMS") + || varName == QLatin1String("FORMS3") + || varName == QLatin1String("RESOURCES")) { + // matches only existent files, expand certain(?) patterns + QStringList vv; + for (int i = v.count(); --i >= 0; ) + vv << expandPattern(v[i]); + v = vv; + } + + if (varName == QLatin1String("TRANSLATIONS")) { + // also matches non-existent files, but does not expand pattern + QString dir = QFileInfo(currentFileName()).absolutePath(); + dir += QLatin1Char('/'); + for (int i = v.count(); --i >= 0; ) + v[i] = QFileInfo(dir, v[i]).absoluteFilePath(); + } + + switch (m_variableOperator) { + case ProVariable::SetOperator: // = + if (!m_cumulative) { + if (!m_skipLevel) { + if (m_isFirstVariableValue) { + m_tempValuemap[varName] = v; + m_tempFilevaluemap[currentProFile()][varName] = v; + } else { // handle lines "CONFIG = foo bar" + m_tempValuemap[varName] += v; + m_tempFilevaluemap[currentProFile()][varName] += v; + } + } + } else { + // We are greedy for values. + m_tempValuemap[varName] += v; + m_tempFilevaluemap[currentProFile()][varName] += v; + } + break; + case ProVariable::UniqueAddOperator: // *= + if (!m_skipLevel || m_cumulative) { + insertUnique(&m_tempValuemap, varName, v); + insertUnique(&m_tempFilevaluemap[currentProFile()], varName, v); + } + break; + case ProVariable::AddOperator: // += + if (!m_skipLevel || m_cumulative) { + m_tempValuemap[varName] += v; + m_tempFilevaluemap[currentProFile()][varName] += v; + } + break; + case ProVariable::RemoveOperator: // -= + if (!m_cumulative) { + if (!m_skipLevel) { + removeEach(&m_tempValuemap, varName, v); + removeEach(&m_tempFilevaluemap[currentProFile()], varName, v); + } + } else { + // We are stingy with our values, too. + } + break; + case ProVariable::ReplaceOperator: // ~= + { + // DEFINES ~= s/a/b/?[gqi] + + // FIXME: qmake variable-expands val first. + if (val.length() < 4 || val[0] != QLatin1Char('s')) { + q->logMessage(format("the ~= operator can handle only the s/// function.")); + return false; + } + QChar sep = val.at(1); + QStringList func = val.split(sep); + if (func.count() < 3 || func.count() > 4) { + q->logMessage(format("the s/// function expects 3 or 4 arguments.")); + return false; + } + + bool global = false, quote = false, case_sense = false; + if (func.count() == 4) { + global = func[3].indexOf(QLatin1Char('g')) != -1; + case_sense = func[3].indexOf(QLatin1Char('i')) == -1; + quote = func[3].indexOf(QLatin1Char('q')) != -1; + } + QString pattern = func[1]; + QString replace = func[2]; + if (quote) + pattern = QRegExp::escape(pattern); + + QRegExp regexp(pattern, case_sense ? Qt::CaseSensitive : Qt::CaseInsensitive); + + if (!m_skipLevel || m_cumulative) { + // We could make a union of modified and unmodified values, + // but this will break just as much as it fixes, so leave it as is. + replaceInList(&m_tempValuemap[varName], regexp, replace, global); + replaceInList(&m_tempFilevaluemap[currentProFile()][varName], regexp, replace, global); + } + } + break; + + } + m_isFirstVariableValue = false; + return true; +} + +bool ProFileEvaluator::Private::visitProFunction(ProFunction *func) +{ + if (!m_updateCondition || m_condition == ConditionFalse) { + QString text = func->text(); + int lparen = text.indexOf(QLatin1Char('(')); + int rparen = text.lastIndexOf(QLatin1Char(')')); + Q_ASSERT(lparen < rparen); + QString arguments = text.mid(lparen + 1, rparen - lparen - 1); + QString funcName = text.left(lparen); + m_lineNo = func->lineNumber(); + bool result; + if (!evaluateConditionalFunction(funcName.trimmed(), arguments, &result)) { + m_invertNext = false; + return false; + } + if (!m_skipLevel && (result ^ m_invertNext)) + m_condition = ConditionTrue; + } + m_invertNext = false; + return true; +} + + +QStringList ProFileEvaluator::Private::qmakeFeaturePaths() +{ + QStringList concat; + { + const QString base_concat = QDir::separator() + QString(QLatin1String("features")); + concat << base_concat + QDir::separator() + QLatin1String("mac"); + concat << base_concat + QDir::separator() + QLatin1String("macx"); + concat << base_concat + QDir::separator() + QLatin1String("unix"); + concat << base_concat + QDir::separator() + QLatin1String("win32"); + concat << base_concat + QDir::separator() + QLatin1String("mac9"); + concat << base_concat + QDir::separator() + QLatin1String("qnx6"); + concat << base_concat; + } + const QString mkspecs_concat = QDir::separator() + QString(QLatin1String("mkspecs")); + QStringList feature_roots; + QByteArray mkspec_path = qgetenv("QMAKEFEATURES"); + if (!mkspec_path.isNull()) + feature_roots += splitPathList(QString::fromLocal8Bit(mkspec_path)); + /* + if (prop) + feature_roots += splitPathList(prop->value("QMAKEFEATURES")); + if (!Option::mkfile::cachefile.isEmpty()) { + QString path; + int last_slash = Option::mkfile::cachefile.lastIndexOf(Option::dir_sep); + if (last_slash != -1) + path = Option::fixPathToLocalOS(Option::mkfile::cachefile.left(last_slash)); + foreach (const QString &concat_it, concat) + feature_roots << (path + concat_it); + } + */ + + QByteArray qmakepath = qgetenv("QMAKEPATH"); + if (!qmakepath.isNull()) { + const QStringList lst = splitPathList(QString::fromLocal8Bit(qmakepath)); + foreach (const QString &item, lst) { + foreach (const QString &concat_it, concat) + feature_roots << (item + mkspecs_concat + concat_it); + } + } + //if (!Option::mkfile::qmakespec.isEmpty()) + // feature_roots << Option::mkfile::qmakespec + QDir::separator() + "features"; + //if (!Option::mkfile::qmakespec.isEmpty()) { + // QFileInfo specfi(Option::mkfile::qmakespec); + // QDir specdir(specfi.absoluteFilePath()); + // while (!specdir.isRoot()) { + // if (!specdir.cdUp() || specdir.isRoot()) + // break; + // if (QFile::exists(specdir.path() + QDir::separator() + "features")) { + // foreach (const QString &concat_it, concat) + // feature_roots << (specdir.path() + concat_it); + // break; + // } + // } + //} + foreach (const QString &concat_it, concat) + feature_roots << (propertyValue(QLatin1String("QT_INSTALL_PREFIX")) + + mkspecs_concat + concat_it); + foreach (const QString &concat_it, concat) + feature_roots << (propertyValue(QLatin1String("QT_INSTALL_DATA")) + + mkspecs_concat + concat_it); + return feature_roots; +} + +QString ProFileEvaluator::Private::propertyValue(const QString &name) const +{ + if (m_properties.contains(name)) + return m_properties.value(name); + if (name == QLatin1String("QT_INSTALL_PREFIX")) + return QLibraryInfo::location(QLibraryInfo::PrefixPath); + if (name == QLatin1String("QT_INSTALL_DATA")) + return QLibraryInfo::location(QLibraryInfo::DataPath); + if (name == QLatin1String("QT_INSTALL_DOCS")) + return QLibraryInfo::location(QLibraryInfo::DocumentationPath); + if (name == QLatin1String("QT_INSTALL_HEADERS")) + return QLibraryInfo::location(QLibraryInfo::HeadersPath); + if (name == QLatin1String("QT_INSTALL_LIBS")) + return QLibraryInfo::location(QLibraryInfo::LibrariesPath); + if (name == QLatin1String("QT_INSTALL_BINS")) + return QLibraryInfo::location(QLibraryInfo::BinariesPath); + if (name == QLatin1String("QT_INSTALL_PLUGINS")) + return QLibraryInfo::location(QLibraryInfo::PluginsPath); + if (name == QLatin1String("QT_INSTALL_TRANSLATIONS")) + return QLibraryInfo::location(QLibraryInfo::TranslationsPath); + if (name == QLatin1String("QT_INSTALL_CONFIGURATION")) + return QLibraryInfo::location(QLibraryInfo::SettingsPath); + if (name == QLatin1String("QT_INSTALL_EXAMPLES")) + return QLibraryInfo::location(QLibraryInfo::ExamplesPath); + if (name == QLatin1String("QT_INSTALL_DEMOS")) + return QLibraryInfo::location(QLibraryInfo::DemosPath); + if (name == QLatin1String("QMAKE_MKSPECS")) + return qmake_mkspec_paths().join(Option::dirlist_sep); + if (name == QLatin1String("QMAKE_VERSION")) + return QLatin1String("1.0"); //### FIXME + //return qmake_version(); +#ifdef QT_VERSION_STR + if (name == QLatin1String("QT_VERSION")) + return QLatin1String(QT_VERSION_STR); +#endif + return QLatin1String("UNKNOWN"); //### +} + +ProFile *ProFileEvaluator::Private::currentProFile() const +{ + if (m_profileStack.count() > 0) + return m_profileStack.top(); + return 0; +} + +QString ProFileEvaluator::Private::currentFileName() const +{ + ProFile *pro = currentProFile(); + if (pro) + return pro->fileName(); + return QString(); +} + +QString ProFileEvaluator::Private::currentDirectory() const +{ + ProFile *cur = m_profileStack.top(); + return cur->directoryName(); +} + +QStringList ProFileEvaluator::Private::expandVariableReferences(const QString &str) +{ + QStringList ret; +// if (ok) +// *ok = true; + if (str.isEmpty()) + return ret; + + const ushort LSQUARE = '['; + const ushort RSQUARE = ']'; + const ushort LCURLY = '{'; + const ushort RCURLY = '}'; + const ushort LPAREN = '('; + const ushort RPAREN = ')'; + const ushort DOLLAR = '$'; + const ushort BACKSLASH = '\\'; + const ushort UNDERSCORE = '_'; + const ushort DOT = '.'; + const ushort SPACE = ' '; + const ushort TAB = '\t'; + const ushort SINGLEQUOTE = '\''; + const ushort DOUBLEQUOTE = '"'; + + ushort unicode, quote = 0; + const QChar *str_data = str.data(); + const int str_len = str.length(); + + ushort term; + QString var, args; + + int replaced = 0; + QString current; + for (int i = 0; i < str_len; ++i) { + unicode = str_data[i].unicode(); + const int start_var = i; + if (unicode == DOLLAR && str_len > i+2) { + unicode = str_data[++i].unicode(); + if (unicode == DOLLAR) { + term = 0; + var.clear(); + args.clear(); + enum { VAR, ENVIRON, FUNCTION, PROPERTY } var_type = VAR; + unicode = str_data[++i].unicode(); + if (unicode == LSQUARE) { + unicode = str_data[++i].unicode(); + term = RSQUARE; + var_type = PROPERTY; + } else if (unicode == LCURLY) { + unicode = str_data[++i].unicode(); + var_type = VAR; + term = RCURLY; + } else if (unicode == LPAREN) { + unicode = str_data[++i].unicode(); + var_type = ENVIRON; + term = RPAREN; + } + forever { + if (!(unicode & (0xFF<<8)) && + unicode != DOT && unicode != UNDERSCORE && + //unicode != SINGLEQUOTE && unicode != DOUBLEQUOTE && + (unicode < 'a' || unicode > 'z') && (unicode < 'A' || unicode > 'Z') && + (unicode < '0' || unicode > '9')) + break; + var.append(QChar(unicode)); + if (++i == str_len) + break; + unicode = str_data[i].unicode(); + // at this point, i points to either the 'term' or 'next' character (which is in unicode) + } + if (var_type == VAR && unicode == LPAREN) { + var_type = FUNCTION; + int depth = 0; + forever { + if (++i == str_len) + break; + unicode = str_data[i].unicode(); + if (unicode == LPAREN) { + depth++; + } else if (unicode == RPAREN) { + if (!depth) + break; + --depth; + } + args.append(QChar(unicode)); + } + if (++i < str_len) + unicode = str_data[i].unicode(); + else + unicode = 0; + // at this point i is pointing to the 'next' character (which is in unicode) + // this might actually be a term character since you can do $${func()} + } + if (term) { + if (unicode != term) { + q->logMessage(format("Missing %1 terminator [found %2]") + .arg(QChar(term)) + .arg(unicode ? QString(unicode) : QString::fromLatin1(("end-of-line")))); +// if (ok) +// *ok = false; + return QStringList(); + } + } else { + // move the 'cursor' back to the last char of the thing we were looking at + --i; + } + // since i never points to the 'next' character, there is no reason for this to be set + unicode = 0; + + QStringList replacement; + if (var_type == ENVIRON) { + replacement = split_value_list(QString::fromLocal8Bit(qgetenv(var.toLatin1().constData()))); + } else if (var_type == PROPERTY) { + replacement << propertyValue(var); + } else if (var_type == FUNCTION) { + replacement << evaluateExpandFunction(var, args); + } else if (var_type == VAR) { + replacement = values(var); + } + if (!(replaced++) && start_var) + current = str.left(start_var); + if (!replacement.isEmpty()) { + if (quote) { + current += replacement.join(QString(Option::field_sep)); + } else { + current += replacement.takeFirst(); + if (!replacement.isEmpty()) { + if (!current.isEmpty()) + ret.append(current); + current = replacement.takeLast(); + if (!replacement.isEmpty()) + ret += replacement; + } + } + } + } else { + if (replaced) + current.append(QLatin1Char('$')); + } + } + if (quote && unicode == quote) { + unicode = 0; + quote = 0; + } else if (unicode == BACKSLASH) { + bool escape = false; + const char *symbols = "[]{}()$\\'\""; + for (const char *s = symbols; *s; ++s) { + if (str_data[i+1].unicode() == (ushort)*s) { + i++; + escape = true; + if (!(replaced++)) + current = str.left(start_var); + current.append(str.at(i)); + break; + } + } + if (escape || !replaced) + unicode =0; + } else if (!quote && (unicode == SINGLEQUOTE || unicode == DOUBLEQUOTE)) { + quote = unicode; + unicode = 0; + if (!(replaced++) && i) + current = str.left(i); + } else if (!quote && (unicode == SPACE || unicode == TAB)) { + unicode = 0; + if (!current.isEmpty()) { + ret.append(current); + current.clear(); + } + } + if (replaced && unicode) + current.append(QChar(unicode)); + } + if (!replaced) + ret = QStringList(str); + else if (!current.isEmpty()) + ret.append(current); + return ret; +} + +bool ProFileEvaluator::Private::isActiveConfig(const QString &config, bool regex) +{ + // magic types for easy flipping + if (config == QLatin1String("true")) + return true; + if (config == QLatin1String("false")) + return false; + + // mkspecs + if ((Option::target_mode == Option::TARG_MACX_MODE + || Option::target_mode == Option::TARG_QNX6_MODE + || Option::target_mode == Option::TARG_UNIX_MODE) + && config == QLatin1String("unix")) + return true; + if (Option::target_mode == Option::TARG_MACX_MODE && config == QLatin1String("macx")) + return true; + if (Option::target_mode == Option::TARG_QNX6_MODE && config == QLatin1String("qnx6")) + return true; + if (Option::target_mode == Option::TARG_MAC9_MODE && config == QLatin1String("mac9")) + return true; + if ((Option::target_mode == Option::TARG_MAC9_MODE + || Option::target_mode == Option::TARG_MACX_MODE) + && config == QLatin1String("mac")) + return true; + if (Option::target_mode == Option::TARG_WIN_MODE && config == QLatin1String("win32")) + return true; + + QRegExp re(config, Qt::CaseSensitive, QRegExp::Wildcard); + QString spec = Option::qmakespec; + if ((regex && re.exactMatch(spec)) || (!regex && spec == config)) + return true; + + return false; +} + +QStringList ProFileEvaluator::Private::evaluateExpandFunction(const QString &func, const QString &arguments) +{ + QStringList argumentsList = split_arg_list(arguments); + QStringList args; + for (int i = 0; i < argumentsList.count(); ++i) + args += expandVariableReferences(argumentsList[i]); + + enum ExpandFunc { E_MEMBER=1, E_FIRST, E_LAST, E_CAT, E_FROMFILE, E_EVAL, E_LIST, + E_SPRINTF, E_JOIN, E_SPLIT, E_BASENAME, E_DIRNAME, E_SECTION, + E_FIND, E_SYSTEM, E_UNIQUE, E_QUOTE, E_ESCAPE_EXPAND, + E_UPPER, E_LOWER, E_FILES, E_PROMPT, E_RE_ESCAPE, + E_REPLACE }; + + static QHash<QString, int> *expands = 0; + if (!expands) { + expands = new QHash<QString, int>; + expands->insert(QLatin1String("member"), E_MEMBER); + expands->insert(QLatin1String("first"), E_FIRST); + expands->insert(QLatin1String("last"), E_LAST); + expands->insert(QLatin1String("cat"), E_CAT); + expands->insert(QLatin1String("fromfile"), E_FROMFILE); // implementation disabled (see comment below) + expands->insert(QLatin1String("eval"), E_EVAL); + expands->insert(QLatin1String("list"), E_LIST); + expands->insert(QLatin1String("sprintf"), E_SPRINTF); + expands->insert(QLatin1String("join"), E_JOIN); + expands->insert(QLatin1String("split"), E_SPLIT); + expands->insert(QLatin1String("basename"), E_BASENAME); + expands->insert(QLatin1String("dirname"), E_DIRNAME); + expands->insert(QLatin1String("section"), E_SECTION); + expands->insert(QLatin1String("find"), E_FIND); + expands->insert(QLatin1String("system"), E_SYSTEM); + expands->insert(QLatin1String("unique"), E_UNIQUE); + expands->insert(QLatin1String("quote"), E_QUOTE); + expands->insert(QLatin1String("escape_expand"), E_ESCAPE_EXPAND); + expands->insert(QLatin1String("upper"), E_UPPER); + expands->insert(QLatin1String("lower"), E_LOWER); + expands->insert(QLatin1String("re_escape"), E_RE_ESCAPE); + expands->insert(QLatin1String("files"), E_FILES); + expands->insert(QLatin1String("prompt"), E_PROMPT); // interactive, so cannot be implemented + expands->insert(QLatin1String("replace"), E_REPLACE); + } + ExpandFunc func_t = ExpandFunc(expands->value(func.toLower())); + + QStringList ret; + + switch (func_t) { + case E_BASENAME: + case E_DIRNAME: + case E_SECTION: { + bool regexp = false; + QString sep, var; + int beg = 0; + int end = -1; + if (func_t == E_SECTION) { + if (args.count() != 3 && args.count() != 4) { + q->logMessage(format("%1(var) section(var, sep, begin, end) " + "requires three or four arguments.").arg(func)); + } else { + var = args[0]; + sep = args[1]; + beg = args[2].toInt(); + if (args.count() == 4) + end = args[3].toInt(); + } + } else { + if (args.count() != 1) { + q->logMessage(format("%1(var) requires one argument.").arg(func)); + } else { + var = args[0]; + regexp = true; + sep = QLatin1String("[\\\\/]"); + if (func_t == E_DIRNAME) + end = -2; + else + beg = -1; + } + } + if (!var.isNull()) { + foreach (const QString str, values(var)) { + if (regexp) + ret += str.section(QRegExp(sep), beg, end); + else + ret += str.section(sep, beg, end); + } + } + break; + } + case E_SPRINTF: + if(args.count() < 1) { + q->logMessage(format("sprintf(format, ...) requires at least one argument")); + } else { + QString tmp = args.at(0); + for (int i = 1; i < args.count(); ++i) + tmp = tmp.arg(args.at(i)); + ret = split_value_list(tmp); + } + break; + case E_JOIN: { + if (args.count() < 1 || args.count() > 4) { + q->logMessage(format("join(var, glue, before, after) requires one to four arguments.")); + } else { + QString glue, before, after; + if (args.count() >= 2) + glue = args[1]; + if (args.count() >= 3) + before = args[2]; + if (args.count() == 4) + after = args[3]; + const QStringList &var = values(args.first()); + if (!var.isEmpty()) + ret.append(before + var.join(glue) + after); + } + break; + } + case E_SPLIT: { + if (args.count() != 2) { + q->logMessage(format("split(var, sep) requires one or two arguments")); + } else { + const QString &sep = (args.count() == 2) ? args[1] : QString(Option::field_sep); + foreach (const QString &var, values(args.first())) + foreach (const QString &splt, var.split(sep)) + ret.append(splt); + } + break; + } + case E_MEMBER: { + if (args.count() < 1 || args.count() > 3) { + q->logMessage(format("member(var, start, end) requires one to three arguments.")); + } else { + bool ok = true; + const QStringList var = values(args.first()); + int start = 0, end = 0; + if (args.count() >= 2) { + QString start_str = args[1]; + start = start_str.toInt(&ok); + if (!ok) { + if (args.count() == 2) { + int dotdot = start_str.indexOf(QLatin1String("..")); + if (dotdot != -1) { + start = start_str.left(dotdot).toInt(&ok); + if (ok) + end = start_str.mid(dotdot+2).toInt(&ok); + } + } + if (!ok) + q->logMessage(format("member() argument 2 (start) '%2' invalid.") + .arg(start_str)); + } else { + end = start; + if (args.count() == 3) + end = args[2].toInt(&ok); + if (!ok) + q->logMessage(format("member() argument 3 (end) '%2' invalid.\n") + .arg(args[2])); + } + } + if (ok) { + if (start < 0) + start += var.count(); + if (end < 0) + end += var.count(); + if (start < 0 || start >= var.count() || end < 0 || end >= var.count()) { + //nothing + } else if (start < end) { + for (int i = start; i <= end && var.count() >= i; i++) + ret.append(var[i]); + } else { + for (int i = start; i >= end && var.count() >= i && i >= 0; i--) + ret += var[i]; + } + } + } + break; + } + case E_FIRST: + case E_LAST: { + if (args.count() != 1) { + q->logMessage(format("%1(var) requires one argument.").arg(func)); + } else { + const QStringList var = values(args.first()); + if (!var.isEmpty()) { + if (func_t == E_FIRST) + ret.append(var[0]); + else + ret.append(var.last()); + } + } + break; + } + case E_CAT: + if (args.count() < 1 || args.count() > 2) { + q->logMessage(format("cat(file, singleline=true) requires one or two arguments.")); + } else { + QString file = args[0]; + file = Option::fixPathToLocalOS(file); + + bool singleLine = true; + if (args.count() > 1) + singleLine = (args[1].toLower() == QLatin1String("true")); + + QFile qfile(file); + if (qfile.open(QIODevice::ReadOnly)) { + QTextStream stream(&qfile); + while (!stream.atEnd()) { + ret += split_value_list(stream.readLine().trimmed()); + if (!singleLine) + ret += QLatin1String("\n"); + } + qfile.close(); + } + } + break; +#if 0 // Used only by Qt's configure for caching + case E_FROMFILE: + if (args.count() != 2) { + q->logMessage(format("fromfile(file, variable) requires two arguments.")); + } else { + QString file = args[0], seek_variableName = args[1]; + + ProFile pro(Option::fixPathToLocalOS(file)); + + ProFileEvaluator visitor; + visitor.setVerbose(m_verbose); + visitor.setCumulative(m_cumulative); + + if (!visitor.queryProFile(&pro)) + break; + + if (!visitor.accept(&pro)) + break; + + ret = visitor.values(seek_variableName); + } + break; +#endif + case E_EVAL: { + if (args.count() != 1) { + q->logMessage(format("eval(variable) requires one argument")); + + } else { + ret += values(args.at(0)); + } + break; } + case E_LIST: { + static int x = 0; + QString tmp; + tmp.sprintf(".QMAKE_INTERNAL_TMP_variableName_%d", x++); + ret = QStringList(tmp); + QStringList lst; + foreach (const QString &arg, args) + lst += split_value_list(arg); + m_valuemap[tmp] = lst; + break; } + case E_FIND: + if (args.count() != 2) { + q->logMessage(format("find(var, str) requires two arguments.")); + } else { + QRegExp regx(args[1]); + foreach (const QString &val, values(args.first())) + if (regx.indexIn(val) != -1) + ret += val; + } + break; + case E_SYSTEM: + if (!m_skipLevel) { + if (args.count() < 1 || args.count() > 2) { + q->logMessage(format("system(execute) requires one or two arguments.")); + } else { + char buff[256]; + FILE *proc = QT_POPEN(args[0].toLatin1(), "r"); + bool singleLine = true; + if (args.count() > 1) + singleLine = (args[1].toLower() == QLatin1String("true")); + QString output; + while (proc && !feof(proc)) { + int read_in = int(fread(buff, 1, 255, proc)); + if (!read_in) + break; + for (int i = 0; i < read_in; i++) { + if ((singleLine && buff[i] == '\n') || buff[i] == '\t') + buff[i] = ' '; + } + buff[read_in] = '\0'; + output += QLatin1String(buff); + } + ret += split_value_list(output); + } + } + break; + case E_UNIQUE: + if(args.count() != 1) { + q->logMessage(format("unique(var) requires one argument.")); + } else { + foreach (const QString &var, values(args.first())) + if (!ret.contains(var)) + ret.append(var); + } + break; + case E_QUOTE: + for (int i = 0; i < args.count(); ++i) + ret += QStringList(args.at(i)); + break; + case E_ESCAPE_EXPAND: + for (int i = 0; i < args.size(); ++i) { + QChar *i_data = args[i].data(); + int i_len = args[i].length(); + for (int x = 0; x < i_len; ++x) { + if (*(i_data+x) == QLatin1Char('\\') && x < i_len-1) { + if (*(i_data+x+1) == QLatin1Char('\\')) { + ++x; + } else { + struct { + char in, out; + } mapped_quotes[] = { + { 'n', '\n' }, + { 't', '\t' }, + { 'r', '\r' }, + { 0, 0 } + }; + for (int i = 0; mapped_quotes[i].in; ++i) { + if (*(i_data+x+1) == QLatin1Char(mapped_quotes[i].in)) { + *(i_data+x) = QLatin1Char(mapped_quotes[i].out); + if (x < i_len-2) + memmove(i_data+x+1, i_data+x+2, (i_len-x-2)*sizeof(QChar)); + --i_len; + break; + } + } + } + } + } + ret.append(QString(i_data, i_len)); + } + break; + case E_RE_ESCAPE: + for (int i = 0; i < args.size(); ++i) + ret += QRegExp::escape(args[i]); + break; + case E_UPPER: + case E_LOWER: + for (int i = 0; i < args.count(); ++i) + if (func_t == E_UPPER) + ret += args[i].toUpper(); + else + ret += args[i].toLower(); + break; + case E_FILES: + if (args.count() != 1 && args.count() != 2) { + q->logMessage(format("files(pattern, recursive=false) requires one or two arguments")); + } else { + bool recursive = false; + if (args.count() == 2) + recursive = (args[1].toLower() == QLatin1String("true") || args[1].toInt()); + QStringList dirs; + QString r = Option::fixPathToLocalOS(args[0]); + int slash = r.lastIndexOf(QDir::separator()); + if (slash != -1) { + dirs.append(r.left(slash)); + r = r.mid(slash+1); + } else { + dirs.append(QString()); + } + + const QRegExp regex(r, Qt::CaseSensitive, QRegExp::Wildcard); + for (int d = 0; d < dirs.count(); d++) { + QString dir = dirs[d]; + if (!dir.isEmpty() && !dir.endsWith(Option::dir_sep)) + dir += QLatin1Char('/'); + + QDir qdir(dir); + for (int i = 0; i < (int)qdir.count(); ++i) { + if (qdir[i] == QLatin1String(".") || qdir[i] == QLatin1String("..")) + continue; + QString fname = dir + qdir[i]; + if (QFileInfo(fname).isDir()) { + if (recursive) + dirs.append(fname); + } + if (regex.exactMatch(qdir[i])) + ret += fname; + } + } + } + break; + case E_REPLACE: + if(args.count() != 3 ) { + q->logMessage(format("replace(var, before, after) requires three arguments")); + } else { + const QRegExp before(args[1]); + const QString after(args[2]); + foreach (QString val, values(args.first())) + ret += val.replace(before, after); + } + break; + case 0: + q->logMessage(format("'%1' is not a recognized replace function").arg(func)); + break; + default: + q->logMessage(format("Function '%1' is not implemented").arg(func)); + break; + } + + return ret; +} + +bool ProFileEvaluator::Private::evaluateConditionalFunction(const QString &function, + const QString &arguments, bool *result) +{ + QStringList argumentsList = split_arg_list(arguments); + QString sep; + sep.append(Option::field_sep); + + QStringList args; + for (int i = 0; i < argumentsList.count(); ++i) + args += expandVariableReferences(argumentsList[i]).join(sep); + + enum TestFunc { T_REQUIRES=1, T_GREATERTHAN, T_LESSTHAN, T_EQUALS, + T_EXISTS, T_EXPORT, T_CLEAR, T_UNSET, T_EVAL, T_CONFIG, T_SYSTEM, + T_RETURN, T_BREAK, T_NEXT, T_DEFINED, T_CONTAINS, T_INFILE, + T_COUNT, T_ISEMPTY, T_INCLUDE, T_LOAD, T_DEBUG, T_MESSAGE, T_IF }; + + static QHash<QString, int> *functions = 0; + if (!functions) { + functions = new QHash<QString, int>; + functions->insert(QLatin1String("requires"), T_REQUIRES); + functions->insert(QLatin1String("greaterThan"), T_GREATERTHAN); + functions->insert(QLatin1String("lessThan"), T_LESSTHAN); + functions->insert(QLatin1String("equals"), T_EQUALS); + functions->insert(QLatin1String("isEqual"), T_EQUALS); + functions->insert(QLatin1String("exists"), T_EXISTS); + functions->insert(QLatin1String("export"), T_EXPORT); + functions->insert(QLatin1String("clear"), T_CLEAR); + functions->insert(QLatin1String("unset"), T_UNSET); + functions->insert(QLatin1String("eval"), T_EVAL); + functions->insert(QLatin1String("CONFIG"), T_CONFIG); + functions->insert(QLatin1String("if"), T_IF); + functions->insert(QLatin1String("isActiveConfig"), T_CONFIG); + functions->insert(QLatin1String("system"), T_SYSTEM); + functions->insert(QLatin1String("return"), T_RETURN); + functions->insert(QLatin1String("break"), T_BREAK); + functions->insert(QLatin1String("next"), T_NEXT); + functions->insert(QLatin1String("defined"), T_DEFINED); + functions->insert(QLatin1String("contains"), T_CONTAINS); + functions->insert(QLatin1String("infile"), T_INFILE); + functions->insert(QLatin1String("count"), T_COUNT); + functions->insert(QLatin1String("isEmpty"), T_ISEMPTY); + functions->insert(QLatin1String("load"), T_LOAD); //v + functions->insert(QLatin1String("include"), T_INCLUDE); //v + functions->insert(QLatin1String("debug"), T_DEBUG); + functions->insert(QLatin1String("message"), T_MESSAGE); //v + functions->insert(QLatin1String("warning"), T_MESSAGE); //v + functions->insert(QLatin1String("error"), T_MESSAGE); //v + } + + bool cond = false; + bool ok = true; + + TestFunc func_t = (TestFunc)functions->value(function); + + switch (func_t) { +#if 0 + case T_INFILE: + case T_REQUIRES: + case T_GREATERTHAN: + case T_LESSTHAN: + case T_EQUALS: + case T_EXPORT: + case T_CLEAR: + case T_UNSET: + case T_EVAL: + case T_IF: + case T_RETURN: + case T_BREAK: + case T_NEXT: + case T_DEFINED: +#endif + case T_CONFIG: { + if (args.count() < 1 || args.count() > 2) { + q->logMessage(format("CONFIG(config) requires one or two arguments.")); + ok = false; + break; + } + if (args.count() == 1) { + //cond = isActiveConfig(args.first()); XXX + break; + } + const QStringList mutuals = args[1].split(QLatin1Char('|')); + const QStringList &configs = valuesDirect(QLatin1String("CONFIG")); + for (int i = configs.size() - 1; i >= 0; i--) { + for (int mut = 0; mut < mutuals.count(); mut++) { + if (configs[i] == mutuals[mut].trimmed()) { + cond = (configs[i] == args[0]); + break; + } + } + } + break; + } + case T_CONTAINS: { + if (args.count() < 2 || args.count() > 3) { + q->logMessage(format("contains(var, val) requires two or three arguments.")); + ok = false; + break; + } + + QRegExp regx(args[1]); + const QStringList &l = values(args.first()); + if (args.count() == 2) { + for (int i = 0; i < l.size(); ++i) { + const QString val = l[i]; + if (regx.exactMatch(val) || val == args[1]) { + cond = true; + break; + } + } + } else { + const QStringList mutuals = args[2].split(QLatin1Char('|')); + for (int i = l.size() - 1; i >= 0; i--) { + const QString val = l[i]; + for (int mut = 0; mut < mutuals.count(); mut++) { + if (val == mutuals[mut].trimmed()) { + cond = (regx.exactMatch(val) || val == args[1]); + break; + } + } + } + } + + break; + } + case T_COUNT: { + if (args.count() != 2 && args.count() != 3) { + q->logMessage(format("count(var, count, op=\"equals\") requires two or three arguments.")); + ok = false; + break; + } + if (args.count() == 3) { + QString comp = args[2]; + if (comp == QLatin1String(">") || comp == QLatin1String("greaterThan")) { + cond = values(args.first()).count() > args[1].toInt(); + } else if (comp == QLatin1String(">=")) { + cond = values(args.first()).count() >= args[1].toInt(); + } else if (comp == QLatin1String("<") || comp == QLatin1String("lessThan")) { + cond = values(args.first()).count() < args[1].toInt(); + } else if (comp == QLatin1String("<=")) { + cond = values(args.first()).count() <= args[1].toInt(); + } else if (comp == QLatin1String("equals") || comp == QLatin1String("isEqual") || comp == QLatin1String("=") || comp == QLatin1String("==")) { + cond = values(args.first()).count() == args[1].toInt(); + } else { + ok = false; + q->logMessage(format("unexpected modifier to count(%2)").arg(comp)); + } + break; + } + cond = values(args.first()).count() == args[1].toInt(); + break; + } + case T_INCLUDE: { + if (m_skipLevel && !m_cumulative) + break; + QString parseInto; + if (args.count() == 2) { + parseInto = args[1]; + } else if (args.count() != 1) { + q->logMessage(format("include(file) requires one or two arguments.")); + ok = false; + break; + } + QString fileName = args.first(); + // ### this breaks if we have include(c:/reallystupid.pri) but IMHO that's really bad style. + QDir currentProPath(currentDirectory()); + fileName = QDir::cleanPath(currentProPath.absoluteFilePath(fileName)); + ok = evaluateFile(fileName, &ok); + break; + } + case T_LOAD: { + if (m_skipLevel && !m_cumulative) + break; + QString parseInto; + bool ignore_error = false; + if (args.count() == 2) { + QString sarg = args[1]; + ignore_error = (sarg.toLower() == QLatin1String("true") || sarg.toInt()); + } else if (args.count() != 1) { + q->logMessage(format("load(feature) requires one or two arguments.")); + ok = false; + break; + } + ok = evaluateFeatureFile( args.first(), &cond); + break; + } + case T_DEBUG: + // Yup - do nothing. Nothing is going to enable debug output anyway. + break; + case T_MESSAGE: { + if (args.count() != 1) { + q->logMessage(format("%1(message) requires one argument.").arg(function)); + ok = false; + break; + } + QString msg = fixEnvVariables(args.first()); + if (function == QLatin1String("error")) { + QStringList parents; + foreach (ProFile *proFile, m_profileStack) + parents.append(proFile->fileName()); + if (!parents.isEmpty()) + parents.takeLast(); + if (parents.isEmpty()) + q->fileMessage(format("Project ERROR: %1").arg(msg)); + else + q->fileMessage(format("Project ERROR: %1. File was included from: '%2'") + .arg(msg).arg(parents.join(QLatin1String("', '")))); + } else { + q->fileMessage(format("Project MESSAGE: %1").arg(msg)); + } + break; + } +#if 0 // Way too dangerous to enable. + case T_SYSTEM: { + if (args.count() != 1) { + q->logMessage(format("system(exec) requires one argument.")); + ok = false; + break; + } + ok = system(args.first().toLatin1().constData()) == 0; + break; + } +#endif + case T_ISEMPTY: { + if (args.count() != 1) { + q->logMessage(format("isEmpty(var) requires one argument.")); + ok = false; + break; + } + QStringList sl = values(args.first()); + if (sl.count() == 0) { + cond = true; + } else if (sl.count() > 0) { + QString var = sl.first(); + cond = (var.isEmpty()); + } + break; + } + case T_EXISTS: { + if (args.count() != 1) { + q->logMessage(format("exists(file) requires one argument.")); + ok = false; + break; + } + QString file = args.first(); + file = Option::fixPathToLocalOS(file); + + if (QFile::exists(file)) { + cond = true; + break; + } + //regular expression I guess + QString dirstr = currentDirectory(); + int slsh = file.lastIndexOf(Option::dir_sep); + if (slsh != -1) { + dirstr = file.left(slsh+1); + file = file.right(file.length() - slsh - 1); + } + if (file.contains(QLatin1Char('*')) || file.contains(QLatin1Char('?'))) + cond = QDir(dirstr).entryList(QStringList(file)).count(); + + break; + } + case 0: + // This is too chatty currently (missing defineTest and defineReplace) + //q->logMessage(format("'%1' is not a recognized test function").arg(function)); + break; + default: + q->logMessage(format("Function '%1' is not implemented").arg(function)); + break; + } + + if (result) + *result = cond; + + return ok; +} + +QStringList ProFileEvaluator::Private::values(const QString &variableName, + const QHash<QString, QStringList> &place, + const ProFile *pro) const +{ + if (variableName == QLatin1String("LITERAL_WHITESPACE")) //a real space in a token + return QStringList(QLatin1String("\t")); + if (variableName == QLatin1String("LITERAL_DOLLAR")) //a real $ + return QStringList(QLatin1String("$")); + if (variableName == QLatin1String("LITERAL_HASH")) //a real # + return QStringList(QLatin1String("#")); + if (variableName == QLatin1String("OUT_PWD")) //the out going dir + return QStringList(m_outputDir); + if (variableName == QLatin1String("PWD") || //current working dir (of _FILE_) + variableName == QLatin1String("IN_PWD")) + return QStringList(currentDirectory()); + if (variableName == QLatin1String("DIR_SEPARATOR")) + return QStringList(Option::dir_sep); + if (variableName == QLatin1String("DIRLIST_SEPARATOR")) + return QStringList(Option::dirlist_sep); + if (variableName == QLatin1String("_LINE_")) //parser line number + return QStringList(QString::number(m_lineNo)); + if (variableName == QLatin1String("_FILE_")) //parser file; qmake is a bit weird here + return QStringList(m_profileStack.size() == 1 ? pro->fileName() : QFileInfo(pro->fileName()).fileName()); + if (variableName == QLatin1String("_DATE_")) //current date/time + return QStringList(QDateTime::currentDateTime().toString()); + if (variableName == QLatin1String("_PRO_FILE_")) + return QStringList(m_origfile); + if (variableName == QLatin1String("_PRO_FILE_PWD_")) + return QStringList(QFileInfo(m_origfile).absolutePath()); + if (variableName == QLatin1String("_QMAKE_CACHE_")) + return QStringList(); // FIXME? + if (variableName.startsWith(QLatin1String("QMAKE_HOST."))) { + QString ret, type = variableName.mid(11); +#if defined(Q_OS_WIN32) + if (type == QLatin1String("os")) { + ret = QLatin1String("Windows"); + } else if (type == QLatin1String("name")) { + DWORD name_length = 1024; + TCHAR name[1024]; + if (GetComputerName(name, &name_length)) + ret = QString::fromUtf16((ushort*)name, name_length); + } else if (type == QLatin1String("version") || type == QLatin1String("version_string")) { + QSysInfo::WinVersion ver = QSysInfo::WindowsVersion; + if (type == QLatin1String("version")) + ret = QString::number(ver); + else if (ver == QSysInfo::WV_Me) + ret = QLatin1String("WinMe"); + else if (ver == QSysInfo::WV_95) + ret = QLatin1String("Win95"); + else if (ver == QSysInfo::WV_98) + ret = QLatin1String("Win98"); + else if (ver == QSysInfo::WV_NT) + ret = QLatin1String("WinNT"); + else if (ver == QSysInfo::WV_2000) + ret = QLatin1String("Win2000"); + else if (ver == QSysInfo::WV_2000) + ret = QLatin1String("Win2003"); + else if (ver == QSysInfo::WV_XP) + ret = QLatin1String("WinXP"); + else if (ver == QSysInfo::WV_VISTA) + ret = QLatin1String("WinVista"); + else + ret = QLatin1String("Unknown"); + } else if (type == QLatin1String("arch")) { + SYSTEM_INFO info; + GetSystemInfo(&info); + switch(info.wProcessorArchitecture) { +#ifdef PROCESSOR_ARCHITECTURE_AMD64 + case PROCESSOR_ARCHITECTURE_AMD64: + ret = QLatin1String("x86_64"); + break; +#endif + case PROCESSOR_ARCHITECTURE_INTEL: + ret = QLatin1String("x86"); + break; + case PROCESSOR_ARCHITECTURE_IA64: +#ifdef PROCESSOR_ARCHITECTURE_IA32_ON_WIN64 + case PROCESSOR_ARCHITECTURE_IA32_ON_WIN64: +#endif + ret = QLatin1String("IA64"); + break; + default: + ret = QLatin1String("Unknown"); + break; + } + } +#elif defined(Q_OS_UNIX) + struct utsname name; + if (!uname(&name)) { + if (type == QLatin1String("os")) + ret = QString::fromLatin1(name.sysname); + else if (type == QLatin1String("name")) + ret = QString::fromLatin1(name.nodename); + else if (type == QLatin1String("version")) + ret = QString::fromLatin1(name.release); + else if (type == QLatin1String("version_string")) + ret = QString::fromLatin1(name.version); + else if (type == QLatin1String("arch")) + ret = QString::fromLatin1(name.machine); + } +#endif + return QStringList(ret); + } + + QStringList result = place[variableName]; + if (result.isEmpty()) { + if (variableName == QLatin1String("TARGET")) { + result.append(QFileInfo(m_origfile).baseName()); + } else if (variableName == QLatin1String("TEMPLATE")) { + result.append(QLatin1String("app")); + } else if (variableName == QLatin1String("QMAKE_DIR_SEP")) { + result.append(Option::dirlist_sep); + } + } + return result; +} + +QStringList ProFileEvaluator::Private::values(const QString &variableName) const +{ + return values(variableName, m_valuemap, currentProFile()); +} + +QStringList ProFileEvaluator::Private::values(const QString &variableName, const ProFile *pro) const +{ + return values(variableName, m_filevaluemap[pro], pro); +} + +ProFile *ProFileEvaluator::parsedProFile(const QString &fileName) +{ + QFileInfo fi(fileName); + if (fi.exists()) { + ProFile *pro = new ProFile(fi.absoluteFilePath()); + if (d->read(pro)) + return pro; + delete pro; + } + return 0; +} + +void ProFileEvaluator::releaseParsedProFile(ProFile *proFile) +{ + delete proFile; +} + +bool ProFileEvaluator::Private::evaluateFile(const QString &fileName, bool *result) +{ + bool ok = true; + ProFile *pro = q->parsedProFile(fileName); + if (pro) { + m_profileStack.push(pro); + ok = (currentProFile() ? pro->Accept(this) : false); + m_profileStack.pop(); + q->releaseParsedProFile(pro); + + if (result) + *result = true; + } else { + if (result) + *result = false; + } +/* if (ok && readFeatures) { + QStringList configs = values("CONFIG"); + QSet<QString> processed; + foreach (const QString &fn, configs) { + if (!processed.contains(fn)) { + processed.insert(fn); + evaluateFeatureFile(fn, 0); + } + } + } */ + + return ok; +} + +bool ProFileEvaluator::Private::evaluateFeatureFile(const QString &fileName, bool *result) +{ + QString fn; + foreach (const QString &path, qmakeFeaturePaths()) { + QString fname = path + QLatin1Char('/') + fileName; + if (QFileInfo(fname).exists()) { + fn = fname; + break; + } + fname += QLatin1String(".prf"); + if (QFileInfo(fname).exists()) { + fn = fname; + break; + } + } + if (fn.isEmpty()) + return false; + bool cumulative = m_cumulative; + m_cumulative = false; + bool ok = evaluateFile(fn, result); + m_cumulative = cumulative; + return ok; +} + +void ProFileEvaluator::Private::expandPatternHelper(const QString &relName, const QString &absName, + QStringList &sources_out) +{ + const QStringList vpaths = values(QLatin1String("VPATH")) + + values(QLatin1String("QMAKE_ABSOLUTE_SOURCE_PATH")) + + values(QLatin1String("DEPENDPATH")) + + values(QLatin1String("VPATH_SOURCES")); + + QFileInfo fi(absName); + bool found = fi.exists(); + // Search in all vpaths + if (!found) { + foreach (const QString &vpath, vpaths) { + fi.setFile(vpath + QDir::separator() + relName); + if (fi.exists()) { + found = true; + break; + } + } + } + + if (found) { + sources_out += fi.absoluteFilePath(); // Not resolving symlinks + } else { + QString val = relName; + QString dir; + QString wildcard = val; + QString real_dir; + if (wildcard.lastIndexOf(QLatin1Char('/')) != -1) { + dir = wildcard.left(wildcard.lastIndexOf(QLatin1Char('/')) + 1); + real_dir = dir; + wildcard = wildcard.right(wildcard.length() - dir.length()); + } + + if (real_dir.isEmpty() || QFileInfo(real_dir).exists()) { + QStringList files = QDir(real_dir).entryList(QStringList(wildcard)); + if (files.isEmpty()) { + q->logMessage(format("Failure to find %1").arg(val)); + } else { + QString a; + for (int i = files.count() - 1; i >= 0; --i) { + if (files[i] == QLatin1String(".") || files[i] == QLatin1String("..")) + continue; + a = dir + files[i]; + sources_out += a; + } + } + } else { + q->logMessage(format("Cannot match %1/%2, as %3 does not exist.") + .arg(real_dir).arg(wildcard).arg(real_dir)); + } + } +} + + +/* + * Lookup of files are done in this order: + * 1. look in pwd + * 2. look in vpaths + * 3. expand wild card files relative from the profiles folder + **/ + +// FIXME: This code supports something that I'd consider a flaw in .pro file syntax +// which is not even documented. So arguably this can be ditched completely... +QStringList ProFileEvaluator::Private::expandPattern(const QString& pattern) +{ + if (!currentProFile()) + return QStringList(); + + QStringList sources_out; + const QString absName = QDir::cleanPath(QDir::current().absoluteFilePath(pattern)); + + expandPatternHelper(pattern, absName, sources_out); + return sources_out; +} + +QString ProFileEvaluator::Private::format(const char *fmt) const +{ + ProFile *pro = currentProFile(); + QString fileName = pro ? pro->fileName() : QLatin1String("Not a file"); + int lineNumber = pro ? m_lineNo : 0; + return QString::fromLatin1("%1(%2):").arg(fileName).arg(lineNumber) + QString::fromAscii(fmt); +} + + +/////////////////////////////////////////////////////////////////////// +// +// ProFileEvaluator +// +/////////////////////////////////////////////////////////////////////// + +ProFileEvaluator::ProFileEvaluator() + : d(new Private(this)) +{ + Option::init(); +} + +ProFileEvaluator::~ProFileEvaluator() +{ + delete d; +} + +bool ProFileEvaluator::contains(const QString &variableName) const +{ + return d->m_valuemap.contains(variableName); +} + +inline QStringList fixEnvVariables(const QStringList &x) +{ + QStringList ret; + foreach (const QString &str, x) + ret << Option::fixString(str, Option::FixEnvVars); + return ret; +} + + +QStringList ProFileEvaluator::values(const QString &variableName) const +{ + return fixEnvVariables(d->values(variableName)); +} + +QStringList ProFileEvaluator::values(const QString &variableName, const ProFile *pro) const +{ + return fixEnvVariables(d->values(variableName, pro)); +} + +ProFileEvaluator::TemplateType ProFileEvaluator::templateType() +{ + QStringList templ = values(QLatin1String("TEMPLATE")); + if (templ.count() >= 1) { + QString t = templ.last().toLower(); + if (t == QLatin1String("app")) + return TT_Application; + if (t == QLatin1String("lib")) + return TT_Library; + if (t == QLatin1String("script")) + return TT_Script; + if (t == QLatin1String("subdirs")) + return TT_Subdirs; + } + return TT_Unknown; +} + +bool ProFileEvaluator::queryProFile(ProFile *pro) +{ + return d->read(pro); +} + +bool ProFileEvaluator::accept(ProFile *pro) +{ + return pro->Accept(d); +} + +QString ProFileEvaluator::propertyValue(const QString &name) const +{ + return d->propertyValue(name); +} + +namespace { + template<class K, class T> void insert(QHash<K,T> *out, const QHash<K,T> &in) + { + typename QHash<K,T>::const_iterator i = in.begin(); + while (i != in.end()) { + out->insert(i.key(), i.value()); + ++i; + } + } +} // anon namespace + +void ProFileEvaluator::addVariables(const QHash<QString, QStringList> &variables) +{ + insert(&(d->m_valuemap), variables); +} + +void ProFileEvaluator::addProperties(const QHash<QString, QString> &properties) +{ + insert(&(d->m_properties), properties); +} + +void ProFileEvaluator::logMessage(const QString &message) +{ + if (d->m_verbose && !d->m_skipLevel) + qWarning("%s", qPrintable(message)); +} + +void ProFileEvaluator::fileMessage(const QString &message) +{ + if (!d->m_skipLevel) + qWarning("%s", qPrintable(message)); +} + +void ProFileEvaluator::errorMessage(const QString &message) +{ + if (!d->m_skipLevel) + qWarning("%s", qPrintable(message)); +} + +void ProFileEvaluator::setVerbose(bool on) +{ + d->m_verbose = on; +} + +void ProFileEvaluator::setCumulative(bool on) +{ + d->m_cumulative = on; +} + +void ProFileEvaluator::setOutputDir(const QString &dir) +{ + d->m_outputDir = dir; +} + +void evaluateProFile(const ProFileEvaluator &visitor, QHash<QByteArray, QStringList> *varMap) +{ + QStringList sourceFiles; + QString codecForTr; + QString codecForSource; + QStringList tsFileNames; + + // app/lib template + sourceFiles += visitor.values(QLatin1String("SOURCES")); + sourceFiles += visitor.values(QLatin1String("HEADERS")); + tsFileNames = visitor.values(QLatin1String("TRANSLATIONS")); + + QStringList trcodec = visitor.values(QLatin1String("CODEC")) + + visitor.values(QLatin1String("DEFAULTCODEC")) + + visitor.values(QLatin1String("CODECFORTR")); + if (!trcodec.isEmpty()) + codecForTr = trcodec.last(); + + QStringList srccodec = visitor.values(QLatin1String("CODECFORSRC")); + if (!srccodec.isEmpty()) + codecForSource = srccodec.last(); + + QStringList forms = visitor.values(QLatin1String("INTERFACES")) + + visitor.values(QLatin1String("FORMS")) + + visitor.values(QLatin1String("FORMS3")); + sourceFiles << forms; + + sourceFiles.sort(); + sourceFiles.removeDuplicates(); + tsFileNames.sort(); + tsFileNames.removeDuplicates(); + + varMap->insert("SOURCES", sourceFiles); + varMap->insert("CODECFORTR", QStringList() << codecForTr); + varMap->insert("CODECFORSRC", QStringList() << codecForSource); + varMap->insert("TRANSLATIONS", tsFileNames); +} + +bool evaluateProFile(const QString &fileName, bool verbose, QHash<QByteArray, QStringList> *varMap) +{ + QFileInfo fi(fileName); + if (!fi.exists()) + return false; + + ProFile pro(fi.absoluteFilePath()); + + ProFileEvaluator visitor; + visitor.setVerbose(verbose); + + if (!visitor.queryProFile(&pro)) + return false; + + if (!visitor.accept(&pro)) + return false; + + evaluateProFile(visitor, varMap); + + return true; +} + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/profileevaluator.h b/tools/linguist/shared/profileevaluator.h new file mode 100644 index 0000000..ae1422a --- /dev/null +++ b/tools/linguist/shared/profileevaluator.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef PROFILEEVALUATOR_H +#define PROFILEEVALUATOR_H + +#include "proitems.h" +#include "abstractproitemvisitor.h" + +#include <QtCore/QIODevice> +#include <QtCore/QHash> +#include <QtCore/QStringList> +#include <QtCore/QStack> + +QT_BEGIN_NAMESPACE + +class ProFile; +class ProFileEvaluator; + +void evaluateProFile(const ProFileEvaluator &visitor, QHash<QByteArray, QStringList> *varMap); +bool evaluateProFile(const QString &fileName, bool verbose, QHash<QByteArray, QStringList> *varMap); + +class ProFileEvaluator +{ +public: + enum TemplateType { + TT_Unknown = 0, + TT_Application, + TT_Library, + TT_Script, + TT_Subdirs + }; + + ProFileEvaluator(); + virtual ~ProFileEvaluator(); + + ProFileEvaluator::TemplateType templateType(); + virtual bool contains(const QString &variableName) const; + void setVerbose(bool on); // Default is false + void setCumulative(bool on); // Default is true! + void setOutputDir(const QString &dir); // Default is empty + + bool queryProFile(ProFile *pro); + bool accept(ProFile *pro); + + void addVariables(const QHash<QString, QStringList> &variables); + void addProperties(const QHash<QString, QString> &properties); + QStringList values(const QString &variableName) const; + QStringList values(const QString &variableName, const ProFile *pro) const; + QString propertyValue(const QString &val) const; + + // for our descendents + virtual ProFile *parsedProFile(const QString &fileName); + virtual void releaseParsedProFile(ProFile *proFile); + virtual void logMessage(const QString &msg); + virtual void errorMessage(const QString &msg); // .pro parse errors + virtual void fileMessage(const QString &msg); // error() and message() from .pro file + +private: + class Private; + Private *d; +}; + +QT_END_NAMESPACE + +#endif // PROFILEEVALUATOR_H diff --git a/tools/linguist/shared/proitems.cpp b/tools/linguist/shared/proitems.cpp new file mode 100644 index 0000000..1895852 --- /dev/null +++ b/tools/linguist/shared/proitems.cpp @@ -0,0 +1,328 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "proitems.h" +#include "abstractproitemvisitor.h" + +#include <QtCore/QFileInfo> + +QT_BEGIN_NAMESPACE + +// --------------- ProItem ------------ +void ProItem::setComment(const QString &comment) +{ + m_comment = comment; +} + +QString ProItem::comment() const +{ + return m_comment; +} + +// --------------- ProBlock ---------------- +ProBlock::ProBlock(ProBlock *parent) +{ + m_blockKind = 0; + m_parent = parent; +} + +ProBlock::~ProBlock() +{ + qDeleteAll(m_proitems); +} + +void ProBlock::appendItem(ProItem *proitem) +{ + m_proitems << proitem; +} + +void ProBlock::setItems(const QList<ProItem *> &proitems) +{ + m_proitems = proitems; +} + +QList<ProItem *> ProBlock::items() const +{ + return m_proitems; +} + +void ProBlock::setBlockKind(int blockKind) +{ + m_blockKind = blockKind; +} + +int ProBlock::blockKind() const +{ + return m_blockKind; +} + +void ProBlock::setParent(ProBlock *parent) +{ + m_parent = parent; +} + +ProBlock *ProBlock::parent() const +{ + return m_parent; +} + +ProItem::ProItemKind ProBlock::kind() const +{ + return ProItem::BlockKind; +} + +bool ProBlock::Accept(AbstractProItemVisitor *visitor) +{ + visitor->visitBeginProBlock(this); + foreach (ProItem *item, m_proitems) { + if (!item->Accept(visitor)) + return false; + } + return visitor->visitEndProBlock(this); +} + +// --------------- ProVariable ---------------- +ProVariable::ProVariable(const QString &name, ProBlock *parent) + : ProBlock(parent) +{ + setBlockKind(ProBlock::VariableKind); + m_variable = name; + m_variableKind = SetOperator; +} + +void ProVariable::setVariableOperator(VariableOperator variableKind) +{ + m_variableKind = variableKind; +} + +ProVariable::VariableOperator ProVariable::variableOperator() const +{ + return m_variableKind; +} + +void ProVariable::setVariable(const QString &name) +{ + m_variable = name; +} + +QString ProVariable::variable() const +{ + return m_variable; +} + +bool ProVariable::Accept(AbstractProItemVisitor *visitor) +{ + visitor->visitBeginProVariable(this); + foreach (ProItem *item, m_proitems) { + if (!item->Accept(visitor)) + return false; + } + return visitor->visitEndProVariable(this); +} + +// --------------- ProValue ---------------- +ProValue::ProValue(const QString &value, ProVariable *variable) +{ + m_variable = variable; + m_value = value; +} + +void ProValue::setValue(const QString &value) +{ + m_value = value; +} + +QString ProValue::value() const +{ + return m_value; +} + +void ProValue::setVariable(ProVariable *variable) +{ + m_variable = variable; +} + +ProVariable *ProValue::variable() const +{ + return m_variable; +} + +ProItem::ProItemKind ProValue::kind() const +{ + return ProItem::ValueKind; +} + +bool ProValue::Accept(AbstractProItemVisitor *visitor) +{ + return visitor->visitProValue(this); +} + +// --------------- ProFunction ---------------- +ProFunction::ProFunction(const QString &text) +{ + m_text = text; +} + +void ProFunction::setText(const QString &text) +{ + m_text = text; +} + +QString ProFunction::text() const +{ + return m_text; +} + +ProItem::ProItemKind ProFunction::kind() const +{ + return ProItem::FunctionKind; +} + +bool ProFunction::Accept(AbstractProItemVisitor *visitor) +{ + return visitor->visitProFunction(this); +} + +// --------------- ProCondition ---------------- +ProCondition::ProCondition(const QString &text) +{ + m_text = text; +} + +void ProCondition::setText(const QString &text) +{ + m_text = text; +} + +QString ProCondition::text() const +{ + return m_text; +} + +ProItem::ProItemKind ProCondition::kind() const +{ + return ProItem::ConditionKind; +} + +bool ProCondition::Accept(AbstractProItemVisitor *visitor) +{ + return visitor->visitProCondition(this); +} + +// --------------- ProOperator ---------------- +ProOperator::ProOperator(OperatorKind operatorKind) +{ + m_operatorKind = operatorKind; +} + +void ProOperator::setOperatorKind(OperatorKind operatorKind) +{ + m_operatorKind = operatorKind; +} + +ProOperator::OperatorKind ProOperator::operatorKind() const +{ + return m_operatorKind; +} + +ProItem::ProItemKind ProOperator::kind() const +{ + return ProItem::OperatorKind; +} + +bool ProOperator::Accept(AbstractProItemVisitor *visitor) +{ + return visitor->visitProOperator(this); +} + +// --------------- ProFile ---------------- +ProFile::ProFile(const QString &fileName) + : ProBlock(0) +{ + m_modified = false; + setBlockKind(ProBlock::ProFileKind); + m_fileName = fileName; + + QFileInfo fi(fileName); + m_displayFileName = fi.fileName(); + m_directoryName = fi.absolutePath(); +} + +ProFile::~ProFile() +{ +} + +QString ProFile::displayFileName() const +{ + return m_displayFileName; +} + +QString ProFile::fileName() const +{ + return m_fileName; +} + +QString ProFile::directoryName() const +{ + return m_directoryName; +} + +void ProFile::setModified(bool modified) +{ + m_modified = modified; +} + +bool ProFile::isModified() const +{ + return m_modified; +} + +bool ProFile::Accept(AbstractProItemVisitor *visitor) +{ + visitor->visitBeginProFile(this); + foreach (ProItem *item, m_proitems) { + if (!item->Accept(visitor)) + return false; + } + return visitor->visitEndProFile(this); +} + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/proitems.h b/tools/linguist/shared/proitems.h new file mode 100644 index 0000000..befaa88 --- /dev/null +++ b/tools/linguist/shared/proitems.h @@ -0,0 +1,236 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef PROITEMS_H +#define PROITEMS_H + +#include <QtCore/QObject> +#include <QtCore/QString> +#include <QtCore/QList> + +QT_BEGIN_NAMESPACE + +struct AbstractProItemVisitor; + +class ProItem +{ +public: + enum ProItemKind { + ValueKind, + FunctionKind, + ConditionKind, + OperatorKind, + BlockKind + }; + + ProItem() : m_lineNumber(0) {} + virtual ~ProItem() {} + + virtual ProItemKind kind() const = 0; + + void setComment(const QString &comment); + QString comment() const; + + virtual bool Accept(AbstractProItemVisitor *visitor) = 0; + int lineNumber() const { return m_lineNumber; } + void setLineNumber(int lineNumber) { m_lineNumber = lineNumber; } + +private: + QString m_comment; + int m_lineNumber; +}; + +class ProBlock : public ProItem +{ +public: + enum ProBlockKind { + NormalKind = 0x00, + ScopeKind = 0x01, + ScopeContentsKind = 0x02, + VariableKind = 0x04, + ProFileKind = 0x08, + SingleLine = 0x10 + }; + + ProBlock(ProBlock *parent); + ~ProBlock(); + + void appendItem(ProItem *proitem); + void setItems(const QList<ProItem *> &proitems); + QList<ProItem *> items() const; + + void setBlockKind(int blockKind); + int blockKind() const; + + void setParent(ProBlock *parent); + ProBlock *parent() const; + + ProItem::ProItemKind kind() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); +protected: + QList<ProItem *> m_proitems; +private: + ProBlock *m_parent; + int m_blockKind; +}; + +class ProVariable : public ProBlock +{ +public: + enum VariableOperator { + AddOperator = 0, + RemoveOperator = 1, + ReplaceOperator = 2, + SetOperator = 3, + UniqueAddOperator = 4 + }; + + ProVariable(const QString &name, ProBlock *parent); + + void setVariableOperator(VariableOperator variableKind); + VariableOperator variableOperator() const; + + void setVariable(const QString &name); + QString variable() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); +private: + VariableOperator m_variableKind; + QString m_variable; +}; + +class ProValue : public ProItem +{ +public: + ProValue(const QString &value, ProVariable *variable); + + void setValue(const QString &value); + QString value() const; + + void setVariable(ProVariable *variable); + ProVariable *variable() const; + + ProItem::ProItemKind kind() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); +private: + QString m_value; + ProVariable *m_variable; +}; + +class ProFunction : public ProItem +{ +public: + explicit ProFunction(const QString &text); + + void setText(const QString &text); + QString text() const; + + ProItem::ProItemKind kind() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); +private: + QString m_text; +}; + +class ProCondition : public ProItem +{ +public: + explicit ProCondition(const QString &text); + + void setText(const QString &text); + QString text() const; + + ProItem::ProItemKind kind() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); +private: + QString m_text; +}; + +class ProOperator : public ProItem +{ +public: + enum OperatorKind { + OrOperator = 1, + NotOperator = 2 + }; + + explicit ProOperator(OperatorKind operatorKind); + + void setOperatorKind(OperatorKind operatorKind); + OperatorKind operatorKind() const; + + ProItem::ProItemKind kind() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); +private: + OperatorKind m_operatorKind; +}; + +class ProFile : public QObject, public ProBlock +{ + Q_OBJECT + +public: + explicit ProFile(const QString &fileName); + ~ProFile(); + + QString displayFileName() const; + QString fileName() const; + QString directoryName() const; + + void setModified(bool modified); + bool isModified() const; + + virtual bool Accept(AbstractProItemVisitor *visitor); + +private: + QString m_fileName; + QString m_displayFileName; + QString m_directoryName; + bool m_modified; +}; + +QT_END_NAMESPACE + +#endif // PROITEMS_H diff --git a/tools/linguist/shared/proparser.pri b/tools/linguist/shared/proparser.pri new file mode 100644 index 0000000..372247e --- /dev/null +++ b/tools/linguist/shared/proparser.pri @@ -0,0 +1,12 @@ + +INCLUDEPATH *= $$PWD + +HEADERS += \ + $$PWD/abstractproitemvisitor.h \ + $$PWD/proitems.h \ + $$PWD/profileevaluator.h \ + $$PWD/proparserutils.h + +SOURCES += \ + $$PWD/proitems.cpp \ + $$PWD/profileevaluator.cpp diff --git a/tools/linguist/shared/proparserutils.h b/tools/linguist/shared/proparserutils.h new file mode 100644 index 0000000..c27c3c0 --- /dev/null +++ b/tools/linguist/shared/proparserutils.h @@ -0,0 +1,299 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef PROPARSERUTILS_H +#define PROPARSERUTILS_H + +#include <QtCore/QDir> +#include <QtCore/QLibraryInfo> + +QT_BEGIN_NAMESPACE + +// Pre- and postcondition macros +#define PRE(cond) do {if (!(cond))qt_assert(#cond,__FILE__,__LINE__);} while (0) +#define POST(cond) do {if (!(cond))qt_assert(#cond,__FILE__,__LINE__);} while (0) + +// This struct is from qmake, but we are not using everything. +struct Option +{ + //simply global convenience + //static QString libtool_ext; + //static QString pkgcfg_ext; + //static QString prf_ext; + //static QString prl_ext; + //static QString ui_ext; + //static QStringList h_ext; + //static QStringList cpp_ext; + //static QString h_moc_ext; + //static QString cpp_moc_ext; + //static QString obj_ext; + //static QString lex_ext; + //static QString yacc_ext; + //static QString h_moc_mod; + //static QString cpp_moc_mod; + //static QString lex_mod; + //static QString yacc_mod; + static QString dir_sep; + static QString dirlist_sep; + static QString qmakespec; + static QChar field_sep; + + enum TARG_MODE { TARG_UNIX_MODE, TARG_WIN_MODE, TARG_MACX_MODE, TARG_MAC9_MODE, TARG_QNX6_MODE }; + static TARG_MODE target_mode; + //static QString pro_ext; + //static QString res_ext; + + static void init() + { +#ifdef Q_OS_WIN + Option::dirlist_sep = QLatin1Char(';'); + Option::dir_sep = QLatin1Char('\\'); +#else + Option::dirlist_sep = QLatin1Char(':'); + Option::dir_sep = QLatin1Char(QLatin1Char('/')); +#endif + Option::qmakespec = QString::fromLatin1(qgetenv("QMAKESPEC").data()); + Option::field_sep = QLatin1Char(' '); + } + + enum StringFixFlags { + FixNone = 0x00, + FixEnvVars = 0x01, + FixPathCanonicalize = 0x02, + FixPathToLocalSeparators = 0x04, + FixPathToTargetSeparators = 0x08 + }; + static QString fixString(QString string, uchar flags); + + inline static QString fixPathToLocalOS(const QString &in, bool fix_env = true, bool canonical = true) + { + uchar flags = FixPathToLocalSeparators; + if (fix_env) + flags |= FixEnvVars; + if (canonical) + flags |= FixPathCanonicalize; + return fixString(in, flags); + } +}; +#if defined(Q_OS_WIN32) +Option::TARG_MODE Option::target_mode = Option::TARG_WIN_MODE; +#elif defined(Q_OS_MAC) +Option::TARG_MODE Option::target_mode = Option::TARG_MACX_MODE; +#elif defined(Q_OS_QNX6) +Option::TARG_MODE Option::target_mode = Option::TARG_QNX6_MODE; +#else +Option::TARG_MODE Option::target_mode = Option::TARG_UNIX_MODE; +#endif + +QString Option::qmakespec; +QString Option::dirlist_sep; +QString Option::dir_sep; +QChar Option::field_sep; + +static void insertUnique(QHash<QString, QStringList> *map, + const QString &key, const QStringList &value) +{ + QStringList &sl = (*map)[key]; + foreach (const QString &str, value) + if (!sl.contains(str)) + sl.append(str); +} + +static void removeEach(QHash<QString, QStringList> *map, + const QString &key, const QStringList &value) +{ + QStringList &sl = (*map)[key]; + foreach (const QString &str, value) + sl.removeAll(str); +} + +/* + See ProFileEvaluator::Private::visitProValue(...) + +static QStringList replaceInList(const QStringList &varList, const QRegExp ®exp, + const QString &replace, bool global) +{ + QStringList resultList = varList; + + for (QStringList::Iterator varit = resultList.begin(); varit != resultList.end();) { + if (varit->contains(regexp)) { + *varit = varit->replace(regexp, replace); + if (varit->isEmpty()) + varit = resultList.erase(varit); + else + ++varit; + if (!global) + break; + } else { + ++varit; + } + } + return resultList; +} +*/ + +inline QString fixEnvVariables(const QString &x) +{ + return Option::fixString(x, Option::FixEnvVars); +} + +inline QStringList splitPathList(const QString &paths) +{ + return paths.split(Option::dirlist_sep); +} + +static QStringList split_arg_list(QString params) +{ + int quote = 0; + QStringList args; + + const ushort LPAREN = '('; + const ushort RPAREN = ')'; + const ushort SINGLEQUOTE = '\''; + const ushort DOUBLEQUOTE = '"'; + const ushort COMMA = ','; + const ushort SPACE = ' '; + //const ushort TAB = '\t'; + + ushort unicode; + const QChar *params_data = params.data(); + const int params_len = params.length(); + int last = 0; + while (last < params_len && ((params_data+last)->unicode() == SPACE + /*|| (params_data+last)->unicode() == TAB*/)) + ++last; + for (int x = last, parens = 0; x <= params_len; x++) { + unicode = (params_data+x)->unicode(); + if (x == params_len) { + while (x && (params_data+(x-1))->unicode() == SPACE) + --x; + QString mid(params_data+last, x-last); + if (quote) { + if (mid[0] == quote && mid[(int)mid.length()-1] == quote) + mid = mid.mid(1, mid.length()-2); + quote = 0; + } + args << mid; + break; + } + if (unicode == LPAREN) { + --parens; + } else if (unicode == RPAREN) { + ++parens; + } else if (quote && unicode == quote) { + quote = 0; + } else if (!quote && (unicode == SINGLEQUOTE || unicode == DOUBLEQUOTE)) { + quote = unicode; + } + if (!parens && !quote && unicode == COMMA) { + QString mid = params.mid(last, x - last).trimmed(); + args << mid; + last = x+1; + while (last < params_len && ((params_data+last)->unicode() == SPACE + /*|| (params_data+last)->unicode() == TAB*/)) + ++last; + } + } + return args; +} + +static QStringList split_value_list(const QString &vals, bool do_semicolon=false) +{ + QString build; + QStringList ret; + QStack<char> quote; + + const ushort LPAREN = '('; + const ushort RPAREN = ')'; + const ushort SINGLEQUOTE = '\''; + const ushort DOUBLEQUOTE = '"'; + const ushort BACKSLASH = '\\'; + const ushort SEMICOLON = ';'; + + ushort unicode; + const QChar *vals_data = vals.data(); + const int vals_len = vals.length(); + for (int x = 0, parens = 0; x < vals_len; x++) { + unicode = vals_data[x].unicode(); + if (x != (int)vals_len-1 && unicode == BACKSLASH && + (vals_data[x+1].unicode() == SINGLEQUOTE || vals_data[x+1].unicode() == DOUBLEQUOTE)) { + build += vals_data[x++]; //get that 'escape' + } else if (!quote.isEmpty() && unicode == quote.top()) { + quote.pop(); + } else if (unicode == SINGLEQUOTE || unicode == DOUBLEQUOTE) { + quote.push(unicode); + } else if (unicode == RPAREN) { + --parens; + } else if (unicode == LPAREN) { + ++parens; + } + + if (!parens && quote.isEmpty() && ((do_semicolon && unicode == SEMICOLON) || + vals_data[x] == Option::field_sep)) { + ret << build; + build.clear(); + } else { + build += vals_data[x]; + } + } + if (!build.isEmpty()) + ret << build; + return ret; +} + +static QStringList qmake_mkspec_paths() +{ + QStringList ret; + const QString concat = QDir::separator() + QString(QLatin1String("mkspecs")); + QByteArray qmakepath = qgetenv("QMAKEPATH"); + if (!qmakepath.isEmpty()) { + const QStringList lst = splitPathList(QString::fromLocal8Bit(qmakepath)); + for (QStringList::ConstIterator it = lst.begin(); it != lst.end(); ++it) + ret << ((*it) + concat); + } + ret << QLibraryInfo::location(QLibraryInfo::DataPath) + concat; + + return ret; +} + +QT_END_NAMESPACE + +#endif // PROPARSERUTILS_H diff --git a/tools/linguist/shared/qm.cpp b/tools/linguist/shared/qm.cpp new file mode 100644 index 0000000..c197e2b --- /dev/null +++ b/tools/linguist/shared/qm.cpp @@ -0,0 +1,717 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QMap> +#include <QtCore/QString> +#include <QtCore/QTextCodec> + +QT_BEGIN_NAMESPACE + +// magic number for the file +static const int MagicLength = 16; +static const uchar magic[MagicLength] = { + 0x3c, 0xb8, 0x64, 0x18, 0xca, 0xef, 0x9c, 0x95, + 0xcd, 0x21, 0x1c, 0xbf, 0x60, 0xa1, 0xbd, 0xdd +}; + + +namespace { + +enum Tag { + Tag_End = 1, + Tag_SourceText16 = 2, + Tag_Translation = 3, + Tag_Context16 = 4, + Tag_Obsolete1 = 5, + Tag_SourceText = 6, + Tag_Context = 7, + Tag_Comment = 8, + Tag_Obsolete2 = 9 +}; + +enum Prefix { + NoPrefix, + Hash, + HashContext, + HashContextSourceText, + HashContextSourceTextComment +}; + +} // namespace anon + +static uint elfHash(const QByteArray &ba) +{ + const uchar *k = (const uchar *)ba.data(); + uint h = 0; + uint g; + + if (k) { + while (*k) { + h = (h << 4) + *k++; + if ((g = (h & 0xf0000000)) != 0) + h ^= g >> 24; + h &= ~g; + } + } + if (!h) + h = 1; + return h; +} + +class Releaser +{ +public: + struct Offset { + Offset() + : h(0), o(0) + {} + Offset(uint hash, uint offset) + : h(hash), o(offset) + {} + + bool operator<(const Offset &other) const { + return (h != other.h) ? h < other.h : o < other.o; + } + bool operator==(const Offset &other) const { + return h == other.h && o == other.o; + } + uint h; + uint o; + }; + + enum { Contexts = 0x2f, Hashes = 0x42, Messages = 0x69, NumerusRules = 0x88 }; + + Releaser() : m_codec(0) {} + + void setCodecName(const QByteArray &codecName) + { + m_codec = QTextCodec::codecForName(codecName); + } + + TranslatorMessage findMessage(const QString &context, + const QString &sourceText, const QString &comment, + const QString &fileName = QString(), int lineNumber = -1) const; + + bool save(QIODevice *iod); + + void insert(const TranslatorMessage &); + void remove(const TranslatorMessage &); + + bool contains(const QString &context, const QString &sourceText, + const QString & comment) const; + + bool contains(const QString &context, const QString &comment, + const QString &fileName, int lineNumber) const; + + void squeeze(TranslatorSaveMode mode); + + QList<TranslatorMessage> messages() const; + + bool isEmpty() const; + + void setNumerusRules(const QByteArray &rules); + +private: + Q_DISABLE_COPY(Releaser) + + // This should reproduce the byte array fetched from the source file, which + // on turn should be the same as passed to the actual tr(...) calls + QByteArray originalBytes(const QString &str, bool isUtf8) const; + + Prefix commonPrefix(const TranslatorMessage &m1, const TranslatorMessage &m2) const; + + uint msgHash(const TranslatorMessage &msg) const; + + void writeMessage(const TranslatorMessage & msg, QDataStream & stream, + TranslatorSaveMode strip, Prefix prefix) const; + + // for squeezed but non-file data, this is what needs to be deleted + QByteArray m_messageArray; + QByteArray m_offsetArray; + QByteArray m_contextArray; + QMap<TranslatorMessage, void *> m_messages; + QByteArray m_numerusRules; + + // Used to reproduce the original bytes + QTextCodec *m_codec; +}; + +QByteArray Releaser::originalBytes(const QString &str, bool isUtf8) const +{ + if (str.isEmpty()) { + // Do not use QByteArray() here as the result of the serialization + // will be different. + return QByteArray(""); + } + if (isUtf8) + return str.toUtf8(); + return m_codec ? m_codec->fromUnicode(str) : str.toLatin1(); +} + +uint Releaser::msgHash(const TranslatorMessage &msg) const +{ + return elfHash(originalBytes(msg.sourceText() + msg.comment(), msg.isUtf8())); +} + +Prefix Releaser::commonPrefix(const TranslatorMessage &m1, const TranslatorMessage &m2) const +{ + if (msgHash(m1) != msgHash(m2)) + return NoPrefix; + if (m1.context() != m2.context()) + return Hash; + if (m1.sourceText() != m2.sourceText()) + return HashContext; + if (m1.comment() != m2.comment()) + return HashContextSourceText; + return HashContextSourceTextComment; +} + +void Releaser::writeMessage(const TranslatorMessage & msg, QDataStream & stream, + TranslatorSaveMode mode, Prefix prefix) const +{ + for (int i = 0; i < msg.translations().count(); ++i) { + QString str = msg.translations().at(i); + str.replace(QChar(Translator::DefaultVariantSeparator), + QChar(Translator::InternalVariantSeparator)); + stream << quint8(Tag_Translation) << str; + } + + if (mode == SaveEverything) + prefix = HashContextSourceTextComment; + + // lrelease produces "wrong" .qm files for QByteArrays that are .isNull(). + switch (prefix) { + default: + case HashContextSourceTextComment: + stream << quint8(Tag_Comment) << originalBytes(msg.comment(), msg.isUtf8()); + // fall through + case HashContextSourceText: + stream << quint8(Tag_SourceText) << originalBytes(msg.sourceText(), msg.isUtf8()); + // fall through + case HashContext: + stream << quint8(Tag_Context) << originalBytes(msg.context(), msg.isUtf8()); + ; + } + + stream << quint8(Tag_End); +} + + +bool Releaser::save(QIODevice *iod) +{ + QDataStream s(iod); + s.writeRawData((const char *)magic, MagicLength); + + if (!m_offsetArray.isEmpty()) { + quint32 oas = quint32(m_offsetArray.size()); + s << quint8(Hashes) << oas; + s.writeRawData(m_offsetArray.constData(), oas); + } + if (!m_messageArray.isEmpty()) { + quint32 mas = quint32(m_messageArray.size()); + s << quint8(Messages) << mas; + s.writeRawData(m_messageArray.constData(), mas); + } + if (!m_contextArray.isEmpty()) { + quint32 cas = quint32(m_contextArray.size()); + s << quint8(Contexts) << cas; + s.writeRawData(m_contextArray.constData(), cas); + } + if (!m_numerusRules.isEmpty()) { + quint32 nrs = m_numerusRules.size(); + s << quint8(NumerusRules) << nrs; + s.writeRawData(m_numerusRules.constData(), nrs); + } + return true; +} + +void Releaser::squeeze(TranslatorSaveMode mode) +{ + if (m_messages.isEmpty() && mode == SaveEverything) + return; + + QMap<TranslatorMessage, void *> messages = m_messages; + + // re-build contents + m_messageArray.clear(); + m_offsetArray.clear(); + m_contextArray.clear(); + m_messages.clear(); + + QMap<Offset, void *> offsets; + + QDataStream ms(&m_messageArray, QIODevice::WriteOnly); + QMap<TranslatorMessage, void *>::const_iterator it, next; + int cpPrev = 0, cpNext = 0; + for (it = messages.constBegin(); it != messages.constEnd(); ++it) { + cpPrev = cpNext; + next = it; + ++next; + if (next == messages.constEnd()) + cpNext = 0; + else + cpNext = commonPrefix(it.key(), next.key()); + offsets.insert(Offset(msgHash(it.key()), ms.device()->pos()), (void *)0); + writeMessage(it.key(), ms, mode, Prefix(qMax(cpPrev, cpNext + 1))); + } + + QMap<Offset, void *>::Iterator offset; + offset = offsets.begin(); + QDataStream ds(&m_offsetArray, QIODevice::WriteOnly); + while (offset != offsets.end()) { + Offset k = offset.key(); + ++offset; + ds << quint32(k.h) << quint32(k.o); + } + + if (mode == SaveStripped) { + QMap<QString, int> contextSet; + for (it = messages.constBegin(); it != messages.constEnd(); ++it) + ++contextSet[it.key().context()]; + + quint16 hTableSize; + if (contextSet.size() < 200) + hTableSize = (contextSet.size() < 60) ? 151 : 503; + else if (contextSet.size() < 2500) + hTableSize = (contextSet.size() < 750) ? 1511 : 5003; + else + hTableSize = (contextSet.size() < 10000) ? 15013 : 3 * contextSet.size() / 2; + + QMultiMap<int, QString> hashMap; + QMap<QString, int>::const_iterator c; + for (c = contextSet.constBegin(); c != contextSet.constEnd(); ++c) + hashMap.insert(elfHash(originalBytes(c.key(), false /*FIXME*/)) % hTableSize, c.key()); + + /* + The contexts found in this translator are stored in a hash + table to provide fast lookup. The context array has the + following format: + + quint16 hTableSize; + quint16 hTable[hTableSize]; + quint8 contextPool[...]; + + The context pool stores the contexts as Pascal strings: + + quint8 len; + quint8 data[len]; + + Let's consider the look-up of context "FunnyDialog". A + hash value between 0 and hTableSize - 1 is computed, say h. + If hTable[h] is 0, "FunnyDialog" is not covered by this + translator. Else, we check in the contextPool at offset + 2 * hTable[h] to see if "FunnyDialog" is one of the + contexts stored there, until we find it or we meet the + empty string. + */ + m_contextArray.resize(2 + (hTableSize << 1)); + QDataStream t(&m_contextArray, QIODevice::WriteOnly); + + quint16 *hTable = new quint16[hTableSize]; + memset(hTable, 0, hTableSize * sizeof(quint16)); + + t << hTableSize; + t.device()->seek(2 + (hTableSize << 1)); + t << quint16(0); // the entry at offset 0 cannot be used + uint upto = 2; + + QMap<int, QString>::const_iterator entry = hashMap.constBegin(); + while (entry != hashMap.constEnd()) { + int i = entry.key(); + hTable[i] = quint16(upto >> 1); + + do { + QString context = entry.value(); + QByteArray ba = context.toUtf8(); + const char *con = ba.data(); + uint len = uint(qstrlen(con)); + len = qMin(len, 255u); + t << quint8(len); + t.writeRawData(con, len); + upto += 1 + len; + ++entry; + } while (entry != hashMap.constEnd() && entry.key() == i); + if (upto & 0x1) { + // offsets have to be even + t << quint8(0); // empty string + ++upto; + } + } + t.device()->seek(2); + for (int j = 0; j < hTableSize; j++) + t << hTable[j]; + delete [] hTable; + + if (upto > 131072) { + qWarning("Releaser::squeeze: Too many contexts"); + m_contextArray.clear(); + } + } +} + +bool Releaser::contains(const QString &context, const QString &sourceText, + const QString &comment) const +{ + return !findMessage(context, sourceText, comment).translation().isNull(); +} + +bool Releaser::contains(const QString &context, const QString &comment, + const QString &fileName, int lineNumber) const +{ + return !findMessage(context, QString(), comment, fileName, lineNumber).isNull(); +} + +void Releaser::insert(const TranslatorMessage &message) +{ + m_messages.insert(message, 0); +} + +void Releaser::remove(const TranslatorMessage &message) +{ + m_messages.remove(message); +} + + +TranslatorMessage Releaser::findMessage(const QString &context, + const QString &sourceText, const QString &comment, + const QString &fileName, int lineNumber) const +{ + if (m_messages.isEmpty()) + return TranslatorMessage(); + + QMap<TranslatorMessage, void *>::const_iterator it; + + // Either we want to find an item that matches context, sourcetext + // (and optionally comment) Or we want to find an item that + // matches context, filename, linenumber (and optionally comment) + TranslatorMessage msg(context, sourceText, comment, QString(), fileName, lineNumber); + it = m_messages.constFind(msg); + if (it != m_messages.constEnd()) + return it.key(); + + if (!comment.isEmpty()) { + it = m_messages.constFind(TranslatorMessage(context, sourceText, QString(), QString(), fileName, lineNumber)); + if (it != m_messages.constEnd()) + return it.key(); + } + + it = m_messages.constFind(TranslatorMessage(context, QString(), comment, QString(), fileName, lineNumber)); + if (it != m_messages.constEnd()) + return it.key(); + if (comment.isEmpty()) + return TranslatorMessage(); + + it = m_messages.constFind(TranslatorMessage(context, QString(), QString(), QString(), fileName, lineNumber)); + if (it != m_messages.constEnd()) + return it.key(); + return TranslatorMessage(); +} + +bool Releaser::isEmpty() const +{ + return m_messageArray.isEmpty() && m_offsetArray.isEmpty() + && m_contextArray.isEmpty() && m_messages.isEmpty(); +} + +void Releaser::setNumerusRules(const QByteArray &rules) +{ + m_numerusRules = rules; +} + +QList<TranslatorMessage> Releaser::messages() const +{ + return m_messages.keys(); +} + +static quint8 read8(const uchar *data) +{ + return *data; +} + +static quint32 read32(const uchar *data) +{ + return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | (data[3]); +} + + +bool loadQM(Translator &translator, QIODevice &dev, ConversionData &cd) +{ + QByteArray ba = dev.readAll(); + const uchar *data = (uchar*)ba.data(); + int len = ba.size(); + if (len < MagicLength || memcmp(data, magic, MagicLength) != 0) { + cd.appendError(QLatin1String("QM-Format error: magic marker missing")); + return false; + } + + enum { Contexts = 0x2f, Hashes = 0x42, Messages = 0x69, NumerusRules = 0x88 }; + + // for squeezed but non-file data, this is what needs to be deleted + const uchar *messageArray = 0; + const uchar *offsetArray = 0; + const uchar *contextArray = 0; + const uchar *numerusRulesArray = 0; + uint messageLength = 0; + uint offsetLength = 0; + uint contextLength = 0; + uint numerusRulesLength = 0; + + bool ok = true; + const uchar *end = data + len; + + data += MagicLength; + + while (data < end - 4) { + quint8 tag = read8(data++); + quint32 blockLen = read32(data); + //qDebug() << "TAG:" << tag << "BLOCKLEN:" << blockLen; + data += 4; + if (!tag || !blockLen) + break; + if (data + blockLen > end) { + ok = false; + break; + } + + if (tag == Contexts) { + contextArray = data; + contextLength = blockLen; + //qDebug() << "CONTEXTS: " << contextLength << QByteArray((const char *)contextArray, contextLength).toHex(); + } else if (tag == Hashes) { + offsetArray = data; + offsetLength = blockLen; + //qDebug() << "HASHES: " << offsetLength << QByteArray((const char *)offsetArray, offsetLength).toHex(); + } else if (tag == Messages) { + messageArray = data; + messageLength = blockLen; + //qDebug() << "MESSAGES: " << messageLength << QByteArray((const char *)messageArray, messageLength).toHex(); + } else if (tag == NumerusRules) { + numerusRulesArray = data; + numerusRulesLength = blockLen; + //qDebug() << "NUMERUSRULES: " << numerusRulesLength << QByteArray((const char *)numerusRulesArray, numerusRulesLength).toHex(); + } + + data += blockLen; + } + + + size_t numItems = offsetLength / (2 * sizeof(quint32)); + //qDebug() << "NUMITEMS: " << numItems; + + TranslatorMessage msg; + + // FIXME: that's just a guess, the original locale data is lost... + QTextCodec *codec = QTextCodec::codecForLocale(); + + for (const uchar *start = offsetArray; start != offsetArray + (numItems << 3); start += 8) { + //quint32 hash = read32(start); + quint32 ro = read32(start + 4); + //qDebug() << "\nHASH:" << hash; + const uchar *m = messageArray + ro; + + for (;;) { + uchar tag = read8(m++); + //qDebug() << "Tag:" << tag << " ADDR: " << m; + switch(tag) { + case Tag_End: + goto end; + case Tag_Translation: { + int len = read32(m); + if (len % 1) { + cd.appendError(QLatin1String("QM-Format error")); + return false; + } + m += 4; + QString str = QString::fromUtf16((const ushort *)m, len/2); + if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { + for (int i = 0; i < str.length(); ++i) + str[i] = QChar((str.at(i).unicode() >> 8) + + ((str.at(i).unicode() << 8) & 0xff00)); + } + str.replace(QChar(Translator::InternalVariantSeparator), + QChar(Translator::DefaultVariantSeparator)); + msg.appendTranslation(str); + m += len; + break; + } + case Tag_Obsolete1: + m += 4; + //qDebug() << "OBSOLETE"; + break; + case Tag_SourceText: { + quint32 len = read32(m); + m += 4; + //qDebug() << "SOURCE LEN: " << len; + //qDebug() << "SOURCE: " << QByteArray((const char*)m, len); + msg.setSourceText(codec->toUnicode(QByteArray((const char*)m, len))); + m += len; + break; + } + case Tag_Context: { + quint32 len = read32(m); + m += 4; + //qDebug() << "CONTEXT LEN: " << len; + //qDebug() << "CONTEXT: " << QByteArray((const char*)m, len); + msg.setContext(codec->toUnicode(QByteArray((const char*)m, len))); + m += len; + break; + } + case Tag_Comment: { + quint32 len = read32(m); + m += 4; + //qDebug() << "COMMENT LEN: " << len; + //qDebug() << "COMMENT: " << QByteArray((const char*)m, len); + msg.setComment(codec->toUnicode(QByteArray((const char*)m, len))); + m += len; + break; + } + default: + //qDebug() << "UNKNOWN TAG" << tag; + break; + } + } + end:; + msg.setType(TranslatorMessage::Finished); + translator.append(msg); + //qDebug() << "\nHASH:" << hash << msg.sourceText() << msg.context(); + msg.setTranslations(QStringList()); + } + return ok; +} + + + +static bool saveQM(const Translator &translator, QIODevice &dev, ConversionData &cd) +{ + Releaser releaser; + QLocale::Language l; + QLocale::Country c; + Translator::languageAndCountry(translator.languageCode(), &l, &c); + QByteArray rules; + if (getNumerusInfo(l, c, &rules, 0)) + releaser.setNumerusRules(rules); + releaser.setCodecName(translator.codecName()); + + int finished = 0; + int unfinished = 0; + int untranslated = 0; + + for (int i = 0; i != translator.messageCount(); ++i) { + const TranslatorMessage &msg = translator.message(i); + TranslatorMessage::Type typ = msg.type(); + if (typ != TranslatorMessage::Obsolete) { + if (typ == TranslatorMessage::Unfinished) { + if (msg.translation().isEmpty()) + ++untranslated; + else + ++unfinished; + } else { + ++finished; + } + QString context = msg.context(); + QString sourceText = msg.sourceText(); + QString comment = msg.comment(); + QStringList translations = msg.translations(); + + if (!cd.ignoreUnfinished() || typ != TranslatorMessage::Unfinished) { + /* + Drop the comment in (context, sourceText, comment), + unless the context is empty, + unless (context, sourceText, "") already exists or + unless we already dropped the comment of (context, + sourceText, comment0). + */ + if (comment.isEmpty() + || context.isEmpty() + || translator.contains(context, sourceText, QString()) + || !releaser.findMessage(context, sourceText, QString()).translation() + .isNull() ) { + releaser.insert(msg); + } else { + TranslatorMessage tm(context, sourceText, QString(), + QString(), QString(), -1, translations); + //filename and lineNumbers will be ignored from now. + releaser.insert(tm); + } + } + } + } + + releaser.squeeze(cd.m_saveMode); + bool saved = releaser.save(&dev); + if (saved && cd.isVerbose()) { + int generatedCount = finished + unfinished; + cd.appendError(QCoreApplication::translate("LRelease", + " Generated %n translation(s) (%1 finished and %2 unfinished)\n", 0, + QCoreApplication::CodecForTr, generatedCount).arg(finished).arg(unfinished)); + if (untranslated) + cd.appendError(QCoreApplication::translate("LRelease", + " Ignored %n untranslated source text(s)\n", 0, + QCoreApplication::CodecForTr, untranslated)); + } + return saved; +} + +int initQM() +{ + Translator::FileFormat format; + + format.extension = QLatin1String("qm"); + format.description = QObject::tr("Compiled Qt translations"); + format.fileType = Translator::FileFormat::TranslationBinary; + format.priority = 0; + format.loader = &loadQM; + format.saver = &saveQM; + Translator::registerFileFormat(format); + + return 1; +} + +Q_CONSTRUCTOR_FUNCTION(initQM) + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/qph.cpp b/tools/linguist/shared/qph.cpp new file mode 100644 index 0000000..6982f09 --- /dev/null +++ b/tools/linguist/shared/qph.cpp @@ -0,0 +1,171 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include <QtCore/QByteArray> +#include <QtCore/QDebug> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> + +#include <QtXml/QXmlStreamReader> +#include <QtXml/QXmlStreamAttribute> + +QT_BEGIN_NAMESPACE + +class QPHReader : public QXmlStreamReader +{ +public: + QPHReader(QIODevice &dev, ConversionData &cd) + : QXmlStreamReader(&dev), m_cd(cd) + {} + + // the "real thing" + bool read(Translator &translator); + +private: + bool elementStarts(const QString &str) const + { + return isStartElement() && name() == str; + } + + bool isWhiteSpace() const + { + return isCharacters() && text().toString().trimmed().isEmpty(); + } + + // needed to expand <byte ... /> + QString readContents(); + // needed to join <lengthvariant>s + QString readTransContents(); + + void handleError(); + + ConversionData &m_cd; + + enum DataField { NoField, SourceField, TargetField, DefinitionField }; + DataField m_currentField; + QString m_currentSource; + QString m_currentTarget; + QString m_currentDefinition; +}; + +bool QPHReader::read(Translator &translator) +{ + m_currentField = NoField; + QString result; + while (!atEnd()) { + readNext(); + if (isStartElement()) { + if (name() == QLatin1String("source")) + m_currentField = SourceField; + else if (name() == QLatin1String("target")) + m_currentField = TargetField; + else if (name() == QLatin1String("definition")) + m_currentField = DefinitionField; + else + m_currentField = NoField; + } else if (isWhiteSpace()) { + // ignore these + } else if (isCharacters()) { + if (m_currentField == SourceField) + m_currentSource += text(); + else if (m_currentField == TargetField) + m_currentTarget += text(); + else if (m_currentField == DefinitionField) + m_currentDefinition += text(); + } else if (isEndElement() && name() == QLatin1String("phrase")) { + TranslatorMessage msg; + msg.setSourceText(m_currentSource); + msg.setTranslation(m_currentTarget); + msg.setTranslatorComment(m_currentDefinition); + translator.append(msg); + m_currentSource.clear(); + m_currentTarget.clear(); + m_currentDefinition.clear(); + } + } + return true; +} + +static bool loadQPH(Translator &translator, QIODevice &dev, ConversionData &cd) +{ + translator.setLocationsType(Translator::NoLocations); + QPHReader reader(dev, cd); + return reader.read(translator); +} + +static bool saveQPH(const Translator &translator, QIODevice &dev, ConversionData &) +{ + QTextStream t(&dev); + t << "<!DOCTYPE QPH><QPH>\n"; + foreach (const TranslatorMessage &msg, translator.messages()) { + t << "<phrase>\n"; + t << " <source>" << msg.sourceText() << "</source>\n"; + t << " <target>" << msg.translations().join(QLatin1String("@")) + << "</target>\n"; + if (!msg.context().isEmpty() || !msg.comment().isEmpty()) + t << " <definition>" << msg.context() << msg.comment() + << "</definition>\n"; + t << "</phrase>\n"; + } + t << "</QPH>\n"; + return true; +} + +int initQPH() +{ + Translator::FileFormat format; + + format.extension = QLatin1String("qph"); + format.description = QObject::tr("Qt Linguist 'Phrase Book'"); + format.fileType = Translator::FileFormat::TranslationSource; + format.priority = 0; + format.loader = &loadQPH; + format.saver = &saveQPH; + Translator::registerFileFormat(format); + + return 1; +} + +Q_CONSTRUCTOR_FUNCTION(initQPH) + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/simtexth.cpp b/tools/linguist/shared/simtexth.cpp new file mode 100644 index 0000000..0556ed6 --- /dev/null +++ b/tools/linguist/shared/simtexth.cpp @@ -0,0 +1,277 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "simtexth.h" +#include "translator.h" + +#include <QtCore/QByteArray> +#include <QtCore/QString> +#include <QtCore/QList> + + +QT_BEGIN_NAMESPACE + +typedef QList<TranslatorMessage> TML; + +/* + How similar are two texts? The approach used here relies on co-occurrence + matrices and is very efficient. + + Let's see with an example: how similar are "here" and "hither"? The + co-occurrence matrix M for "here" is M[h,e] = 1, M[e,r] = 1, M[r,e] = 1, and 0 + elsewhere; the matrix N for "hither" is N[h,i] = 1, N[i,t] = 1, ..., + N[h,e] = 1, N[e,r] = 1, and 0 elsewhere. The union U of both matrices is the + matrix U[i,j] = max { M[i,j], N[i,j] }, and the intersection V is + V[i,j] = min { M[i,j], N[i,j] }. The score for a pair of texts is + + score = (sum of V[i,j] over all i, j) / (sum of U[i,j] over all i, j), + + a formula suggested by Arnt Gulbrandsen. Here we have + + score = 2 / 6, + + or one third. + + The implementation differs from this in a few details. Most importantly, + repetitions are ignored; for input "xxx", M[x,x] equals 1, not 2. +*/ + +/* + Every character is assigned to one of 20 buckets so that the co-occurrence + matrix requires only 20 * 20 = 400 bits, not 256 * 256 = 65536 bits or even + more if we want the whole Unicode. Which character falls in which bucket is + arbitrary. + + The second half of the table is a replica of the first half, because of + laziness. +*/ +static const int indexOf[256] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +// ! " # $ % & ' ( ) * + , - . / + 0, 2, 6, 7, 10, 12, 15, 19, 2, 6, 7, 10, 12, 15, 19, 0, +// 0 1 2 3 4 5 6 7 8 9 : ; < = > ? + 1, 3, 4, 5, 8, 9, 11, 13, 14, 16, 2, 6, 7, 10, 12, 15, +// @ A B C D E F G H I J K L M N O + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 10, 11, 12, 13, 14, +// P Q R S T U V W X Y Z [ \ ] ^ _ + 15, 12, 16, 17, 18, 19, 2, 10, 15, 7, 19, 2, 6, 7, 10, 0, +// ` a b c d e f g h i j k l m n o + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 10, 11, 12, 13, 14, +// p q r s t u v w x y z { | } ~ + 15, 12, 16, 17, 18, 19, 2, 10, 15, 7, 19, 2, 6, 7, 10, 0, + + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 6, 7, 10, 12, 15, 19, 2, 6, 7, 10, 12, 15, 19, 0, + 1, 3, 4, 5, 8, 9, 11, 13, 14, 16, 2, 6, 7, 10, 12, 15, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 10, 11, 12, 13, 14, + 15, 12, 16, 17, 18, 19, 2, 10, 15, 7, 19, 2, 6, 7, 10, 0, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 10, 11, 12, 13, 14, + 15, 12, 16, 17, 18, 19, 2, 10, 15, 7, 19, 2, 6, 7, 10, 0 +}; + +/* + The entry bitCount[i] (for i between 0 and 255) is the number of bits used to + represent i in binary. +*/ +static const int bitCount[256] = { + 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, + 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8 +}; + +struct CoMatrix +{ + /* + The matrix has 20 * 20 = 400 entries. This requires 50 bytes, or 13 + words. Some operations are performed on words for more efficiency. + */ + union { + quint8 b[52]; + quint32 w[13]; + }; + + CoMatrix() { memset( b, 0, 52 ); } + + CoMatrix(const QString &str) + { + QByteArray ba = str.toUtf8(); + const char *text = ba.constData(); + char c = '\0', d; + memset( b, 0, 52 ); + /* + The Knuth books are not in the office only for show; they help make + loops 30% faster and 20% as readable. + */ + while ( (d = *text) != '\0' ) { + setCoOccurence( c, d ); + if ( (c = *++text) != '\0' ) { + setCoOccurence( d, c ); + text++; + } + } + } + + void setCoOccurence( char c, char d ) { + int k = indexOf[(uchar) c] + 20 * indexOf[(uchar) d]; + b[k >> 3] |= (1 << (k & 0x7)); + } + + int worth() const { + int w = 0; + for ( int i = 0; i < 50; i++ ) + w += bitCount[b[i]]; + return w; + } +}; + +static inline CoMatrix reunion(const CoMatrix &m, const CoMatrix &n) +{ + CoMatrix p; + for (int i = 0; i < 13; ++i) + p.w[i] = m.w[i] | n.w[i]; + return p; +} + +static inline CoMatrix intersection(const CoMatrix &m, const CoMatrix &n) +{ + CoMatrix p; + for (int i = 0; i < 13; ++i) + p.w[i] = m.w[i] & n.w[i]; + return p; +} + +StringSimilarityMatcher::StringSimilarityMatcher(const QString &stringToMatch) +{ + m_cm = new CoMatrix(stringToMatch); + m_length = stringToMatch.length(); +} + +int StringSimilarityMatcher::getSimilarityScore(const QString &strCandidate) +{ + CoMatrix cmTarget(strCandidate); + int delta = qAbs(m_length - strCandidate.size()); + int score = ( (intersection(*m_cm, cmTarget).worth() + 1) << 10 ) / + ( reunion(*m_cm, cmTarget).worth() + (delta << 1) + 1 ); + return score; +} + +StringSimilarityMatcher::~StringSimilarityMatcher() +{ + delete m_cm; +} + +/** + * Checks how similar two strings are. + * The return value is the score, and a higher score is more similar + * than one with a low score. + * Linguist considers a score over 190 to be a good match. + * \sa StringSimilarityMatcher + */ +int getSimilarityScore(const QString &str1, const QString &str2) +{ + CoMatrix cmTarget(str2); + CoMatrix cm(str1); + int delta = qAbs(str1.size() - str2.size()); + + int score = ( (intersection(cm, cmTarget).worth() + 1) << 10 ) + / ( reunion(cm, cmTarget).worth() + (delta << 1) + 1 ); + + return score; +} + +CandidateList similarTextHeuristicCandidates(const Translator *tor, + const QString &text, int maxCandidates) +{ + QList<int> scores; + CandidateList candidates; + + TML all = tor->translatedMessages(); + + foreach (const TranslatorMessage &mtm, all) { + if (mtm.type() == TranslatorMessage::Unfinished + || mtm.translation().isEmpty()) + continue; + + QString s = mtm.sourceText(); + int score = getSimilarityScore(s, text); + + if (candidates.size() == maxCandidates && score > scores[maxCandidates - 1] ) + candidates.removeLast(); + + if (candidates.size() < maxCandidates && score >= textSimilarityThreshold) { + Candidate cand( s, mtm.translation() ); + + int i; + for (i = 0; i < candidates.size(); i++) { + if (score >= scores.at(i)) { + if (score == scores.at(i)) { + if (candidates.at(i) == cand) + goto continue_outer_loop; + } else { + break; + } + } + } + scores.insert(i, score); + candidates.insert(i, cand); + } + continue_outer_loop: + ; + } + return candidates; +} + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/simtexth.h b/tools/linguist/shared/simtexth.h new file mode 100644 index 0000000..e3cad91 --- /dev/null +++ b/tools/linguist/shared/simtexth.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef SIMTEXTH_H +#define SIMTEXTH_H + +const int textSimilarityThreshold = 190; + +#include <QString> +#include <QList> + +QT_BEGIN_NAMESPACE + +class Translator; + +struct Candidate +{ + Candidate() {} + Candidate(const QString& source0, const QString &target0) + : source(source0), target(target0) + {} + + QString source; + QString target; +}; + +inline bool operator==( const Candidate& c, const Candidate& d ) { + return c.target == d.target && c.source == d.source; +} +inline bool operator!=( const Candidate& c, const Candidate& d ) { + return !operator==( c, d ); +} + +typedef QList<Candidate> CandidateList; + +struct CoMatrix; +/** + * This class is more efficient for searching through a large array of candidate strings, since we only + * have to construct the CoMatrix for the \a stringToMatch once, + * after that we just call getSimilarityScore(strCandidate). + * \sa getSimilarityScore + */ +class StringSimilarityMatcher { +public: + StringSimilarityMatcher(const QString &stringToMatch); + ~StringSimilarityMatcher(); + int getSimilarityScore(const QString &strCandidate); + +private: + CoMatrix *m_cm; + int m_length; +}; + +int getSimilarityScore(const QString &str1, const QString &str2); + +CandidateList similarTextHeuristicCandidates( const Translator *tor, + const QString &text, + int maxCandidates ); + +QT_END_NAMESPACE + +#endif diff --git a/tools/linguist/shared/translator.cpp b/tools/linguist/shared/translator.cpp new file mode 100644 index 0000000..3b5e8f3 --- /dev/null +++ b/tools/linguist/shared/translator.cpp @@ -0,0 +1,559 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include "simtexth.h" + +#include <stdio.h> + +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> + +#include <private/qtranslator_p.h> + +QT_BEGIN_NAMESPACE + +Translator::Translator() : + m_codecName("ISO-8859-1"), + m_locationsType(AbsoluteLocations) +{ +} + +void Translator::registerFileFormat(const FileFormat &format) +{ + //qDebug() << "Translator: Registering format " << format.extension; + QList<Translator::FileFormat> &formats = registeredFileFormats(); + for (int i = 0; i < formats.size(); ++i) + if (format.fileType == formats[i].fileType && format.priority < formats[i].priority) { + formats.insert(i, format); + return; + } + formats.append(format); +} + +QList<Translator::FileFormat> &Translator::registeredFileFormats() +{ + static QList<Translator::FileFormat> theFormats; + return theFormats; +} + +void Translator::replace(const TranslatorMessage &msg) +{ + int index = m_messages.indexOf(msg); + if (index == -1) + m_messages.append(msg); + else + m_messages[index] = msg; +} + +void Translator::replaceSorted(const TranslatorMessage &msg) +{ + int index = m_messages.indexOf(msg); + if (index == -1) + appendSorted(msg); + else + m_messages[index] = msg; +} + +void Translator::extend(const TranslatorMessage &msg) +{ + int index = m_messages.indexOf(msg); + if (index == -1) { + m_messages.append(msg); + } else { + m_messages[index].addReferenceUniq(msg.fileName(), msg.lineNumber()); + if (!msg.extraComment().isEmpty()) { + QString cmt = m_messages[index].extraComment(); + if (!cmt.isEmpty()) + cmt.append(QLatin1String("\n----------\n")); + cmt.append(msg.extraComment()); + m_messages[index].setExtraComment(cmt); + } + } +} + +void Translator::append(const TranslatorMessage &msg) +{ + m_messages.append(msg); +} + +void Translator::appendSorted(const TranslatorMessage &msg) +{ + int msgLine = msg.lineNumber(); + if (msgLine < 0) { + m_messages.append(msg); + return; + } + + int bestIdx = 0; // Best insertion point found so far + int bestScore = 0; // Its category: 0 = no hit, 1 = pre or post, 2 = middle + int bestSize = 0; // The length of the region. Longer is better within one category. + + // The insertion point to use should this region turn out to be the best one so far + int thisIdx = 0; + int thisScore = 0; + int thisSize = 0; + // Working vars + int prevLine = 0; + int curIdx = 0; + foreach (const TranslatorMessage &mit, m_messages) { + bool sameFile = mit.fileName() == msg.fileName(); + int curLine; + if (sameFile && (curLine = mit.lineNumber()) >= prevLine) { + if (msgLine >= prevLine && msgLine < curLine) { + thisIdx = curIdx; + thisScore = thisSize ? 2 : 1; + } + ++thisSize; + prevLine = curLine; + } else { + if (thisSize) { + if (!thisScore) { + thisIdx = curIdx; + thisScore = 1; + } + if (thisScore > bestScore || (thisScore == bestScore && thisSize > bestSize)) { + bestIdx = thisIdx; + bestScore = thisScore; + bestSize = thisSize; + } + thisScore = 0; + thisSize = sameFile ? 1 : 0; + prevLine = 0; + } + } + ++curIdx; + } + if (thisSize && !thisScore) { + thisIdx = curIdx; + thisScore = 1; + } + if (thisScore > bestScore || (thisScore == bestScore && thisSize > bestSize)) + m_messages.insert(thisIdx, msg); + else if (bestScore) + m_messages.insert(bestIdx, msg); + else + m_messages.append(msg); +} + +static QString guessFormat(const QString &filename, const QString &format) +{ + if (format != QLatin1String("auto")) + return format; + + foreach (const Translator::FileFormat &fmt, Translator::registeredFileFormats()) { + if (filename.endsWith(QLatin1Char('.') + fmt.extension, Qt::CaseInsensitive)) + return fmt.extension; + } + + // the default format. + // FIXME: change to something more widely distributed later. + return QLatin1String("ts"); +} + +bool Translator::load(const QString &filename, ConversionData &cd, const QString &format) +{ + cd.m_sourceDir = QFileInfo(filename).absoluteDir(); + cd.m_sourceFileName = filename; + + QFile file; + if (filename.isEmpty() || filename == QLatin1String("-")) { + if (!file.open(stdin, QIODevice::ReadOnly)) { + cd.appendError(QString::fromLatin1("Cannot open stdin!? (%1)") + .arg(file.errorString())); + return false; + } + } else { + file.setFileName(filename); + if (!file.open(QIODevice::ReadOnly)) { + cd.appendError(QString::fromLatin1("Cannot open %1: %2") + .arg(filename, file.errorString())); + return false; + } + } + + QString fmt = guessFormat(filename, format); + + foreach (const FileFormat &format, registeredFileFormats()) { + if (fmt == format.extension) { + if (format.loader) + return (*format.loader)(*this, file, cd); + cd.appendError(QString(QLatin1String("No loader for format %1 found")) + .arg(fmt)); + return false; + } + } + + cd.appendError(QString(QLatin1String("Unknown format %1 for file %2")) + .arg(format, filename)); + return false; +} + + +bool Translator::save(const QString &filename, ConversionData &cd, const QString &format) const +{ + QFile file; + if (filename.isEmpty() || filename == QLatin1String("-")) { + if (!file.open(stdout, QIODevice::WriteOnly)) { + cd.appendError(QString::fromLatin1("Cannot open stdout!? (%1)") + .arg(file.errorString())); + return false; + } + } else { + file.setFileName(filename); + if (!file.open(QIODevice::WriteOnly)) { + cd.appendError(QString::fromLatin1("Cannot create %1: %2") + .arg(filename, file.errorString())); + return false; + } + } + + QString fmt = guessFormat(filename, format); + cd.m_targetDir = QFileInfo(filename).absoluteDir(); + + foreach (const FileFormat &format, registeredFileFormats()) { + if (fmt == format.extension) { + if (format.saver) + return (*format.saver)(*this, file, cd); + cd.appendError(QString(QLatin1String("Cannot save %1 files")).arg(fmt)); + return false; + } + } + + cd.appendError(QString(QLatin1String("Unknown format %1 for file %2")) + .arg(format).arg(filename)); + return false; +} + +QString Translator::makeLanguageCode(QLocale::Language language, QLocale::Country country) +{ + QLocale locale(language, country); + if (country == QLocale::AnyCountry) { + QString languageCode = locale.name().section(QLatin1Char('_'), 0, 0); + if (languageCode.length() <= 3) + return languageCode; + return QString(); + } else { + return locale.name(); + } +} + +void Translator::languageAndCountry(const QString &languageCode, + QLocale::Language *lang, QLocale::Country *country) +{ + QLocale locale(languageCode); + if (lang) + *lang = locale.language(); + + if (country) { + if (languageCode.indexOf(QLatin1Char('_')) != -1) + *country = locale.country(); + else + *country = QLocale::AnyCountry; + } +} + +bool Translator::release(QFile *iod, ConversionData &cd) const +{ + foreach (const FileFormat &format, registeredFileFormats()) { + if (format.extension == QLatin1String("qm")) + return (*format.saver)(*this, *iod, cd); + } + cd.appendError(QLatin1String("No .qm saver available.")); + return false; +} + +bool Translator::contains(const QString &context, + const QString &sourceText, const QString &comment) const +{ + return m_messages.contains(TranslatorMessage(context, sourceText, comment, + QString(), QString(), 0)); +} + +TranslatorMessage Translator::find(const QString &context, + const QString &sourceText, const QString &comment) const +{ + TranslatorMessage needle(context, sourceText, comment, QString(), QString(), 0); + int index = m_messages.indexOf(needle); + return index == -1 ? TranslatorMessage() : m_messages.at(index); +} + +TranslatorMessage Translator::find(const QString &context, + const QString &comment, const TranslatorMessage::References &refs) const +{ + if (!refs.isEmpty()) { + for (TMM::ConstIterator it = m_messages.constBegin(); it != m_messages.constEnd(); ++it) { + if (it->context() == context && it->comment() == comment) + foreach (const TranslatorMessage::Reference &itref, it->allReferences()) + foreach (const TranslatorMessage::Reference &ref, refs) + if (itref == ref) + return *it; + } + } + return TranslatorMessage(); +} + +bool Translator::contains(const QString &context) const +{ + foreach (const TranslatorMessage &msg, m_messages) + if (msg.context() == context && msg.sourceText().isEmpty()) + return true; + return false; +} + +TranslatorMessage Translator::find(const QString &context) const +{ + foreach (const TranslatorMessage &msg, m_messages) + if (msg.context() == context && msg.sourceText().isEmpty()) + return msg; + return TranslatorMessage(); +} + +void Translator::stripObsoleteMessages() +{ + TMM newmm; + for (TMM::ConstIterator it = m_messages.begin(); it != m_messages.end(); ++it) + if (it->type() != TranslatorMessage::Obsolete) + newmm.append(*it); + m_messages = newmm; +} + +void Translator::stripFinishedMessages() +{ + TMM newmm; + for (TMM::ConstIterator it = m_messages.begin(); it != m_messages.end(); ++it) + if (it->type() != TranslatorMessage::Finished) + newmm.append(*it); + m_messages = newmm; +} + +void Translator::stripEmptyContexts() +{ + TMM newmm; + for (TMM::ConstIterator it = m_messages.begin(); it != m_messages.end(); ++it) + if (it->sourceText() != QLatin1String(ContextComment)) + newmm.append(*it); + m_messages = newmm; +} + +void Translator::stripNonPluralForms() +{ + TMM newmm; + for (TMM::ConstIterator it = m_messages.begin(); it != m_messages.end(); ++it) + if (it->isPlural()) + newmm.append(*it); + m_messages = newmm; +} + +void Translator::stripIdenticalSourceTranslations() +{ + TMM newmm; + for (TMM::ConstIterator it = m_messages.begin(); it != m_messages.end(); ++it) { + // we need to have just one translation, and it be equal to the source + if (it->translations().count() != 1) + newmm.append(*it); + else if (it->translation() != it->sourceText()) + newmm.append(*it); + } + m_messages = newmm; +} + +void Translator::dropTranslations() +{ + for (TMM::Iterator it = m_messages.begin(); it != m_messages.end(); ++it) { + if (it->type() == TranslatorMessage::Finished) + it->setType(TranslatorMessage::Unfinished); + it->setTranslation(QString()); + } +} + +QList<TranslatorMessage> Translator::findDuplicates() const +{ + QHash<TranslatorMessage, int> dups; + foreach (const TranslatorMessage &msg, m_messages) + dups[msg]++; + QList<TranslatorMessage> ret; + QHash<TranslatorMessage, int>::ConstIterator it = dups.constBegin(), end = dups.constEnd(); + for (; it != end; ++it) + if (it.value() > 1) + ret.append(it.key()); + return ret; +} + +// Used by lupdate to be able to search using absolute paths during merging +void Translator::makeFileNamesAbsolute(const QDir &originalPath) +{ + TMM newmm; + for (TMM::iterator it = m_messages.begin(); it != m_messages.end(); ++it) { + TranslatorMessage msg = *it; + msg.setReferences(TranslatorMessage::References()); + foreach (const TranslatorMessage::Reference &ref, it->allReferences()) { + QString fileName = ref.fileName(); + QFileInfo fi (fileName); + if (fi.isRelative()) + fileName = originalPath.absoluteFilePath(fileName); + msg.addReference(fileName, ref.lineNumber()); + } + newmm.append(msg); + } + m_messages = newmm; +} + +QList<TranslatorMessage> Translator::messages() const +{ + return m_messages; +} + +QList<TranslatorMessage> Translator::translatedMessages() const +{ + TMM result; + for (TMM::ConstIterator it = m_messages.begin(); it != m_messages.end(); ++it) + if (it->type() == TranslatorMessage::Finished) + result.append(*it); + return result; +} + +QStringList Translator::normalizedTranslations(const TranslatorMessage &msg, + QLocale::Language language, QLocale::Country country) +{ + QStringList translations = msg.translations(); + int numTranslations = 1; + if (msg.isPlural() && language != QLocale::C) { + QStringList forms; + if (getNumerusInfo(language, country, 0, &forms)) + numTranslations = forms.count(); // includes singular + } + + // make sure that the stringlist always have the size of the + // language's current numerus, or 1 if its not plural + if (translations.count() > numTranslations) { + for (int i = translations.count(); i > numTranslations; --i) + translations.removeLast(); + } else if (translations.count() < numTranslations) { + for (int i = translations.count(); i < numTranslations; ++i) + translations.append(QString()); + } + return translations; +} + +QStringList Translator::normalizedTranslations(const TranslatorMessage &msg, + ConversionData &cd, bool *ok) const +{ + QLocale::Language l; + QLocale::Country c; + languageAndCountry(languageCode(), &l, &c); + QStringList translns = normalizedTranslations(msg, l, c); + if (msg.translations().size() > translns.size() && ok) { + cd.appendError(QLatin1String( + "Removed plural forms as the target language has less " + "forms.\nIf this sounds wrong, possibly the target language is " + "not set or recognized.\n")); + *ok = false; + } + return translns; +} + +QString Translator::guessLanguageCodeFromFileName(const QString &filename) +{ + QString str = filename; + foreach (const FileFormat &format, registeredFileFormats()) { + if (str.endsWith(format.extension)) { + str = str.left(str.size() - format.extension.size() - 1); + break; + } + } + static QRegExp re(QLatin1String("[\\._]")); + while (true) { + QLocale locale(str); + //qDebug() << "LANGUAGE FROM " << str << "LANG: " << locale.language(); + if (locale.language() != QLocale::C) { + //qDebug() << "FOUND " << locale.name(); + return locale.name(); + } + int pos = str.indexOf(re); + if (pos == -1) + break; + str = str.mid(pos + 1); + } + //qDebug() << "LANGUAGE GUESSING UNSUCCESSFUL"; + return QString(); +} + +bool Translator::hasExtra(const QString &key) const +{ + return m_extra.contains(key); +} + +QString Translator::extra(const QString &key) const +{ + return m_extra[key]; +} + +void Translator::setExtra(const QString &key, const QString &value) +{ + m_extra[key] = value; +} + +void Translator::setCodecName(const QByteArray &name) +{ + QTextCodec *codec = QTextCodec::codecForName(name); + if (!codec) { + if (!name.isEmpty()) + qWarning("No QTextCodec for %s available. Using Latin1\n", name.constData()); + m_codecName.clear(); + } else { + m_codecName = codec->name(); + } +} + +void Translator::dump() const +{ + for (int i = 0; i != messageCount(); ++i) + message(i).dump(); +} + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/translator.h b/tools/linguist/shared/translator.h new file mode 100644 index 0000000..de98e9d --- /dev/null +++ b/tools/linguist/shared/translator.h @@ -0,0 +1,229 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef METATRANSLATOR_H +#define METATRANSLATOR_H + +#include "translatormessage.h" + +#include <QDir> +#include <QList> +#include <QLocale> +#include <QMultiHash> +#include <QString> +#include <QSet> + + +QT_BEGIN_NAMESPACE + +Q_DECLARE_TYPEINFO(TranslatorMessage, Q_MOVABLE_TYPE); + +class QIODevice; + +// A struct of "interesting" data passed to and from the load and save routines +class ConversionData +{ +public: + ConversionData() : + m_verbose(false), + m_ignoreUnfinished(false), + m_sortContexts(false), + m_noUiLines(false), + m_saveMode(SaveEverything) + {} + + // tag manipulation + const QStringList &dropTags() const { return m_dropTags; } + QStringList &dropTags() { return m_dropTags; } + const QDir &targetDir() const { return m_targetDir; } + bool isVerbose() const { return m_verbose; } + bool ignoreUnfinished() const { return m_ignoreUnfinished; } + bool sortContexts() const { return m_sortContexts; } + + void appendError(const QString &error) { m_errors.append(error); } + QString error() const { return m_errors.join(QLatin1String("\n")); } + QStringList errors() const { return m_errors; } + void clearErrors() { m_errors.clear(); } + +public: + QString m_defaultContext; + QByteArray m_codecForSource; // CPP specific + QString m_sourceFileName; + QString m_targetFileName; + QDir m_sourceDir; + QDir m_targetDir; // FIXME: TS specific + QSet<QString> m_projectRoots; + QMultiHash<QString, QString> m_allCSources; + QStringList m_includePath; + QStringList m_dropTags; // tags to be dropped + QStringList m_errors; + bool m_verbose; + bool m_ignoreUnfinished; + bool m_sortContexts; + bool m_noUiLines; + TranslatorSaveMode m_saveMode; +}; + +class Translator +{ +public: + Translator(); + + bool load(const QString &filename, ConversionData &err, const QString &format /*= "auto"*/); + bool save(const QString &filename, ConversionData &err, const QString &format /*= "auto"*/) const; + bool release(QFile *iod, ConversionData &cd) const; + + bool contains(const QString &context, const QString &sourceText, + const QString &comment) const; + TranslatorMessage find(const QString &context, + const QString &sourceText, const QString &comment) const; + + TranslatorMessage find(const QString &context, + const QString &comment, const TranslatorMessage::References &refs) const; + + bool contains(const QString &context) const; + TranslatorMessage find(const QString &context) const; + + void replace(const TranslatorMessage &msg); + void replaceSorted(const TranslatorMessage &msg); + void extend(const TranslatorMessage &msg); // Only for single-location messages + void append(const TranslatorMessage &msg); + void appendSorted(const TranslatorMessage &msg); + + void stripObsoleteMessages(); + void stripFinishedMessages(); + void stripEmptyContexts(); + void stripNonPluralForms(); + void stripIdenticalSourceTranslations(); + void dropTranslations(); + QList<TranslatorMessage> findDuplicates() const; + void makeFileNamesAbsolute(const QDir &originalPath); + + void setCodecName(const QByteArray &name); + QByteArray codecName() const { return m_codecName; } + + QString languageCode() const { return m_language; } + QString sourceLanguageCode() const { return m_sourceLanguage; } + + enum LocationsType { NoLocations, RelativeLocations, AbsoluteLocations }; + void setLocationsType(LocationsType lt) { m_locationsType = lt; } + LocationsType locationsType() const { return m_locationsType; } + + static QString makeLanguageCode(QLocale::Language language, QLocale::Country country); + static void languageAndCountry(const QString &languageCode, + QLocale::Language *lang, QLocale::Country *country); + void setLanguageCode(const QString &languageCode) { m_language = languageCode; } + void setSourceLanguageCode(const QString &languageCode) { m_sourceLanguage = languageCode; } + static QString guessLanguageCodeFromFileName(const QString &fileName); + QList<TranslatorMessage> messages() const; + QList<TranslatorMessage> translatedMessages() const; + static QStringList normalizedTranslations(const TranslatorMessage &m, + QLocale::Language lang, QLocale::Country country); + QStringList normalizedTranslations(const TranslatorMessage &m, ConversionData &cd, bool *ok) const; + + int messageCount() const { return m_messages.size(); } + TranslatorMessage &message(int i) { return m_messages[i]; } + const TranslatorMessage &message(int i) const { return m_messages.at(i); } + void dump() const; + + // additional file format specific data + // note: use '<fileformat>:' as prefix for file format specific members, + // e.g. "po-flags", "po-msgid_plural" + typedef TranslatorMessage::ExtraData ExtraData; + QString extra(const QString &ba) const; + void setExtra(const QString &ba, const QString &var); + bool hasExtra(const QString &ba) const; + const ExtraData &extras() const { return m_extra; } + void setExtras(const ExtraData &extras) { m_extra = extras; } + + // registration of file formats + typedef bool (*SaveFunction)(const Translator &, QIODevice &out, ConversionData &data); + typedef bool (*LoadFunction)(Translator &, QIODevice &in, ConversionData &data); + struct FileFormat { + FileFormat() : loader(0), saver(0), priority(-1) {} + QString extension; // such as "ts", "xlf", ... + QString description; // human-readable description + LoadFunction loader; + SaveFunction saver; + enum FileType { TranslationSource, TranslationBinary } fileType; + int priority; // 0 = highest, -1 = invisible + }; + static void registerFileFormat(const FileFormat &format); + static QList<FileFormat> ®isteredFileFormats(); + + enum VariantSeparators { + DefaultVariantSeparator = 0x2762, // some weird character nobody ever heard of :-D + InternalVariantSeparator = 0x9c // unicode "STRING TERMINATOR" + }; + +private: + typedef QList<TranslatorMessage> TMM; // int stores the sequence position. + + TMM m_messages; + QByteArray m_codecName; + LocationsType m_locationsType; + + // A string beginning with a 2 or 3 letter language code (ISO 639-1 + // or ISO-639-2), followed by the optional country variant to distinguish + // between country-specific variations of the language. The language code + // and country code are always separated by '_' + // Note that the language part can also be a 3-letter ISO 639-2 code. + // Legal examples: + // 'pt' portuguese, assumes portuguese from portugal + // 'pt_BR' Brazilian portuguese (ISO 639-1 language code) + // 'por_BR' Brazilian portuguese (ISO 639-2 language code) + QString m_language; + QString m_sourceLanguage; + ExtraData m_extra; +}; + +bool getNumerusInfo(QLocale::Language language, QLocale::Country country, + QByteArray *rules, QStringList *forms); + +/* + This is a quick hack. The proper way to handle this would be + to extend Translator's interface. +*/ +#define ContextComment "QT_LINGUIST_INTERNAL_CONTEXT_COMMENT" + +QT_END_NAMESPACE + +#endif diff --git a/tools/linguist/shared/translatormessage.cpp b/tools/linguist/shared/translatormessage.cpp new file mode 100644 index 0000000..ab4301f --- /dev/null +++ b/tools/linguist/shared/translatormessage.cpp @@ -0,0 +1,217 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translatormessage.h" + +#include <qplatformdefs.h> + +#ifndef QT_NO_TRANSLATION + +#include <QDataStream> +#include <QDebug> + +#include <stdlib.h> + + +QT_BEGIN_NAMESPACE + +TranslatorMessage::TranslatorMessage() + : m_lineNumber(-1), m_type(Unfinished), m_utf8(false), m_plural(false) +{ +} + +TranslatorMessage::TranslatorMessage(const QString &context, + const QString &sourceText, const QString &comment, + const QString &userData, + const QString &fileName, int lineNumber, const QStringList &translations, + Type type, bool plural) + : m_context(context), m_sourcetext(sourceText), m_comment(comment), + m_userData(userData), + m_translations(translations), m_fileName(fileName), m_lineNumber(lineNumber), + m_type(type), m_utf8(false), m_plural(plural) +{ +} + +void TranslatorMessage::addReference(const QString &fileName, int lineNumber) +{ + if (m_fileName.isEmpty()) { + m_fileName = fileName; + m_lineNumber = lineNumber; + } else { + m_extraRefs.append(Reference(fileName, lineNumber)); + } +} + +void TranslatorMessage::addReferenceUniq(const QString &fileName, int lineNumber) +{ + if (m_fileName.isEmpty()) { + m_fileName = fileName; + m_lineNumber = lineNumber; + } else { + if (fileName == m_fileName && lineNumber == m_lineNumber) + return; + if (!m_extraRefs.isEmpty()) // Rather common case, so special-case it + foreach (const Reference &ref, m_extraRefs) + if (fileName == ref.fileName() && lineNumber == ref.lineNumber()) + return; + m_extraRefs.append(Reference(fileName, lineNumber)); + } +} + +void TranslatorMessage::clearReferences() +{ + m_fileName.clear(); + m_lineNumber = -1; + m_extraRefs.clear(); +} + +void TranslatorMessage::setReferences(const TranslatorMessage::References &refs0) +{ + if (!refs0.isEmpty()) { + References refs = refs0; + const Reference &ref = refs.takeFirst(); + m_fileName = ref.fileName(); + m_lineNumber = ref.lineNumber(); + m_extraRefs = refs; + } else { + clearReferences(); + } +} + +TranslatorMessage::References TranslatorMessage::allReferences() const +{ + References refs; + if (!m_fileName.isEmpty()) { + refs.append(Reference(m_fileName, m_lineNumber)); + refs += m_extraRefs; + } + return refs; +} + +static bool needs8BitHelper(const QString &ba) +{ + for (int i = ba.size(); --i >= 0; ) + if (ba.at(i).unicode() >= 0x80) + return true; + return false; +} + +bool TranslatorMessage::needs8Bit() const +{ + //dump(); + return needs8BitHelper(m_sourcetext) + || needs8BitHelper(m_comment) + || needs8BitHelper(m_context); +} + + +bool TranslatorMessage::operator==(const TranslatorMessage& m) const +{ + static QString msgIdPlural = QLatin1String("po-msgid_plural"); + + // Special treatment for context comments (empty source). + return (m_context == m.m_context) + && m_sourcetext == m.m_sourcetext + && m_extra[msgIdPlural] == m.m_extra[msgIdPlural] + && (m_sourcetext.isEmpty() || m_comment == m.m_comment); +} + + +bool TranslatorMessage::operator<(const TranslatorMessage& m) const +{ + if (m_context != m.m_context) + return m_context < m.m_context; + if (m_sourcetext != m.m_sourcetext) + return m_sourcetext < m.m_sourcetext; + return m_comment < m.m_comment; +} + +int qHash(const TranslatorMessage &msg) +{ + return + qHash(msg.context()) ^ + qHash(msg.sourceText()) ^ + qHash(msg.extra(QLatin1String("po-msgid_plural"))) ^ + qHash(msg.comment()); +} + +bool TranslatorMessage::hasExtra(const QString &key) const +{ + return m_extra.contains(key); +} + +QString TranslatorMessage::extra(const QString &key) const +{ + return m_extra[key]; +} + +void TranslatorMessage::setExtra(const QString &key, const QString &value) +{ + m_extra[key] = value; +} + +void TranslatorMessage::unsetExtra(const QString &key) +{ + m_extra.remove(key); +} + +void TranslatorMessage::dump() const +{ + qDebug() + << "\nId : " << m_id + << "\nContext : " << m_context + << "\nSource : " << m_sourcetext + << "\nComment : " << m_comment + << "\nUserData : " << m_userData + << "\nExtraComment : " << m_extraComment + << "\nTranslatorComment : " << m_translatorComment + << "\nTranslations : " << m_translations + << "\nFileName : " << m_fileName + << "\nLineNumber : " << m_lineNumber + << "\nType : " << m_type + << "\nPlural : " << m_plural + << "\nExtra : " << m_extra; +} + + +QT_END_NAMESPACE + +#endif // QT_NO_TRANSLATION diff --git a/tools/linguist/shared/translatormessage.h b/tools/linguist/shared/translatormessage.h new file mode 100644 index 0000000..fa37ff5 --- /dev/null +++ b/tools/linguist/shared/translatormessage.h @@ -0,0 +1,181 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TRANSLATORMESSAGE_H +#define TRANSLATORMESSAGE_H + +#ifndef QT_NO_TRANSLATION + +#include <QString> +#include <QStringList> +#include <QHash> + + +QT_BEGIN_NAMESPACE + +enum TranslatorSaveMode { SaveEverything, SaveStripped }; + +class TranslatorMessage +{ +public: + enum Type { Unfinished, Finished, Obsolete }; + typedef QHash<QString, QString> ExtraData; + class Reference + { + QString m_fileName; + int m_lineNumber; + public: + Reference(const QString &n, int l) : m_fileName(n), m_lineNumber(l) {} + bool operator==(const Reference &other) const + { return fileName() == other.fileName() && lineNumber() == other.lineNumber(); } + QString fileName() const { return m_fileName; } + int lineNumber() const { return m_lineNumber; } + }; + typedef QList<Reference> References; + + TranslatorMessage(); + TranslatorMessage(const QString &context, const QString &sourceText, + const QString &comment, const QString &userData, + const QString &fileName, int lineNumber, + const QStringList &translations = QStringList(), + Type type = Unfinished, bool plural = false); + + uint hash() const; + + QString id() const { return m_id; } + void setId(const QString &id) { m_id = id; } + + QString context() const { return m_context; } + void setContext(const QString &context) { m_context = context; } + + QString sourceText() const { return m_sourcetext; } + void setSourceText(const QString &sourcetext) { m_sourcetext = sourcetext; } + QString oldSourceText() const { return m_oldsourcetext; } + void setOldSourceText(const QString &oldsourcetext) { m_oldsourcetext = oldsourcetext; } + + QString comment() const { return m_comment; } + void setComment(const QString &comment) { m_comment = comment; } + QString oldComment() const { return m_oldcomment; } + void setOldComment(const QString &oldcomment) { m_oldcomment = oldcomment; } + + QStringList translations() const { return m_translations; } + void setTranslations(const QStringList &translations) { m_translations = translations; } + QString translation() const { return m_translations.value(0); } + void setTranslation(const QString &translation) { m_translations = QStringList(translation); } + void appendTranslation(const QString &translation) { m_translations.append(translation); } + bool isTranslated() const + { + foreach (const QString &trans, m_translations) + if (!trans.isEmpty()) + return true; + return false; + } + + bool operator==(const TranslatorMessage& m) const; + bool operator<(const TranslatorMessage& m) const; + + QString fileName() const { return m_fileName; } + void setFileName(const QString &fileName) { m_fileName = fileName; } + int lineNumber() const { return m_lineNumber; } + void setLineNumber(int lineNumber) { m_lineNumber = lineNumber; } + void clearReferences(); + void setReferences(const References &refs); + void addReference(const QString &fileName, int lineNumber); + void addReference(const Reference &ref) { addReference(ref.fileName(), ref.lineNumber()); } + void addReferenceUniq(const QString &fileName, int lineNumber); + References extraReferences() const { return m_extraRefs; } + References allReferences() const; + QString userData() const { return m_userData; } + void setUserData(const QString &userData) { m_userData = userData; } + QString extraComment() const { return m_extraComment; } + void setExtraComment(const QString &extraComment) { m_extraComment = extraComment; } + QString translatorComment() const { return m_translatorComment; } + void setTranslatorComment(const QString &translatorComment) { m_translatorComment = translatorComment; } + + bool isNull() const { return m_sourcetext.isNull() && m_lineNumber == -1 && m_translations.isEmpty(); } + + Type type() const { return m_type; } + void setType(Type t) { m_type = t; } + bool isUtf8() const { return m_utf8; } // codecForTr override + void setUtf8(bool on) { m_utf8 = on; } + bool isPlural() const { return m_plural; } + void setPlural(bool isplural) { m_plural = isplural; } + + // note: use '<fileformat>:' as prefix for file format specific members, + // e.g. "po-msgid_plural" + QString extra(const QString &ba) const; + void setExtra(const QString &ba, const QString &var); + bool hasExtra(const QString &ba) const; + const ExtraData &extras() const { return m_extra; } + void setExtras(const ExtraData &extras) { m_extra = extras; } + void unsetExtra(const QString &key); + + bool needs8Bit() const; + void dump() const; + +private: + QString m_id; + QString m_context; + QString m_sourcetext; + QString m_oldsourcetext; + QString m_comment; + QString m_oldcomment; + QString m_userData; + ExtraData m_extra; // PO flags, PO plurals + QString m_extraComment; + QString m_translatorComment; + QStringList m_translations; + QString m_fileName; + int m_lineNumber; + References m_extraRefs; + + Type m_type; + bool m_utf8; + bool m_plural; +}; + +int qHash(const TranslatorMessage &msg); + +QT_END_NAMESPACE + +#endif // QT_NO_TRANSLATION + +#endif // TRANSLATORMESSAGE_H diff --git a/tools/linguist/shared/ts.cpp b/tools/linguist/shared/ts.cpp new file mode 100644 index 0000000..2e7d40f --- /dev/null +++ b/tools/linguist/shared/ts.cpp @@ -0,0 +1,755 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include <QtCore/QByteArray> +#include <QtCore/QDebug> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> + +#include <QtXml/QXmlStreamReader> +#include <QtXml/QXmlStreamAttribute> + +#define STRINGIFY_INTERNAL(x) #x +#define STRINGIFY(x) STRINGIFY_INTERNAL(x) +#define STRING(s) static QString str##s(QLatin1String(STRINGIFY(s))) + +QT_BEGIN_NAMESPACE + +/* + * The encodings are a total mess. + * A Translator has a codecForTr(). Each message's text will be passed to tr() + * in that encoding or as UTF-8 to trUtf8() if it is flagged as such. + * For ts 2.0, the file content is always uniformly in UTF-8. The file stores + * the codecForTr default and marks deviating messages accordingly. + * For ts 1.1, the file content is in mixed encoding. Each message is encoded + * the way it will be passed to tr() (with 8-bit characters encoded as numeric + * entities) or trUtf8(). The file stores the encoding and codecForTr in one + * attribute, for both the default and each deviating message. + */ + + +QDebug &operator<<(QDebug &d, const QXmlStreamAttribute &attr) +{ + return d << "[" << attr.name().toString() << "," << attr.value().toString() << "]"; +} + + +class TSReader : public QXmlStreamReader +{ +public: + TSReader(QIODevice &dev, ConversionData &cd) + : QXmlStreamReader(&dev), m_cd(cd) + {} + + // the "real thing" + bool read(Translator &translator); + +private: + bool elementStarts(const QString &str) const + { + return isStartElement() && name() == str; + } + + bool isWhiteSpace() const + { + return isCharacters() && text().toString().trimmed().isEmpty(); + } + + // needed to expand <byte ... /> + QString readContents(); + // needed to join <lengthvariant>s + QString readTransContents(); + + void handleError(); + + ConversionData &m_cd; +}; + +void TSReader::handleError() +{ + if (isComment()) + return; + if (hasError() && error() == CustomError) // raised by readContents + return; + + const QString loc = QString::fromLatin1("at %3:%1:%2") + .arg(lineNumber()).arg(columnNumber()).arg(m_cd.m_sourceFileName); + + switch (tokenType()) { + case NoToken: // Cannot happen + default: // likewise + case Invalid: + raiseError(QString::fromLatin1("Parse error %1: %2").arg(loc, errorString())); + break; + case StartElement: + raiseError(QString::fromLatin1("Unexpected tag <%1> %2").arg(name().toString(), loc)); + break; + case Characters: + { + QString tok = text().toString(); + if (tok.length() > 30) + tok = tok.left(30) + QLatin1String("[...]"); + raiseError(QString::fromLatin1("Unexpected characters '%1' %2").arg(tok, loc)); + } + break; + case EntityReference: + raiseError(QString::fromLatin1("Unexpected entity '&%1;' %2").arg(name().toString(), loc)); + break; + case ProcessingInstruction: + raiseError(QString::fromLatin1("Unexpected processing instruction %1").arg(loc)); + break; + } +} + +static QString byteValue(QString value) +{ + int base = 10; + if (value.startsWith(QLatin1String("x"))) { + base = 16; + value.remove(0, 1); + } + int n = value.toUInt(0, base); + return (n != 0) ? QString(QChar(n)) : QString(); +} + +QString TSReader::readContents() +{ + STRING(byte); + STRING(value); + + QString result; + while (!atEnd()) { + readNext(); + if (isEndElement()) { + break; + } else if (isCharacters()) { + result += text(); + } else if (elementStarts(strbyte)) { + // <byte value="..."> + result += byteValue(attributes().value(strvalue).toString()); + readNext(); + if (!isEndElement()) { + handleError(); + break; + } + } else { + handleError(); + break; + } + } + //qDebug() << "TEXT: " << result; + return result; +} + +QString TSReader::readTransContents() +{ + STRING(lengthvariant); + STRING(variants); + STRING(yes); + + if (attributes().value(strvariants) == stryes) { + QString result; + while (!atEnd()) { + readNext(); + if (isEndElement()) { + break; + } else if (isWhiteSpace()) { + // ignore these, just whitespace + } else if (elementStarts(strlengthvariant)) { + if (!result.isEmpty()) + result += QChar(Translator::DefaultVariantSeparator); + result += readContents(); + } else { + handleError(); + break; + } + } + return result; + } else { + return readContents(); + } +} + +bool TSReader::read(Translator &translator) +{ + STRING(byte); + STRING(comment); + STRING(context); + STRING(defaultcodec); + STRING(encoding); + STRING(extracomment); + STRING(filename); + STRING(id); + STRING(language); + STRING(line); + STRING(location); + STRING(message); + STRING(name); + STRING(numerus); + STRING(numerusform); + STRING(obsolete); + STRING(oldcomment); + STRING(oldsource); + STRING(source); + STRING(sourcelanguage); + STRING(translation); + STRING(translatorcomment); + STRING(true); + STRING(TS); + STRING(type); + STRING(unfinished); + STRING(userdata); + STRING(utf8); + STRING(value); + STRING(version); + STRING(yes); + + static const QString strextrans(QLatin1String("extra-")); + static const QString strUtf8(QLatin1String("UTF-8")); + + while (!atEnd()) { + readNext(); + if (isStartDocument()) { + // <!DOCTYPE TS> + //qDebug() << attributes(); + } else if (isEndDocument()) { + // <!DOCTYPE TS> + //qDebug() << attributes(); + } else if (isDTD()) { + // <!DOCTYPE TS> + //qDebug() << tokenString(); + } else if (elementStarts(strTS)) { + // <TS> + //qDebug() << "TS " << attributes(); + QHash<QString, int> currentLine; + QString currentFile; + + QXmlStreamAttributes atts = attributes(); + //QString version = atts.value(strversion).toString(); + translator.setLanguageCode(atts.value(strlanguage).toString()); + translator.setSourceLanguageCode(atts.value(strsourcelanguage).toString()); + while (!atEnd()) { + readNext(); + if (isEndElement()) { + // </TS> found, finish local loop + break; + } else if (isWhiteSpace()) { + // ignore these, just whitespace + } else if (elementStarts(strdefaultcodec)) { + // <defaultcodec> + translator.setCodecName(readElementText().toLatin1()); + // </defaultcodec> + } else if (isStartElement() + && name().toString().startsWith(strextrans)) { + // <extra-...> + QString tag = name().toString(); + translator.setExtra(tag.mid(6), readContents()); + // </extra-...> + } else if (elementStarts(strcontext)) { + // <context> + QString context; + while (!atEnd()) { + readNext(); + if (isEndElement()) { + // </context> found, finish local loop + break; + } else if (isWhiteSpace()) { + // ignore these, just whitespace + } else if (elementStarts(strname)) { + // <name> + context = readElementText(); + // </name> + } else if (elementStarts(strmessage)) { + // <message> + TranslatorMessage::References refs; + QString currentMsgFile = currentFile; + + TranslatorMessage msg; + msg.setId(attributes().value(strid).toString()); + msg.setContext(context); + msg.setType(TranslatorMessage::Finished); + msg.setPlural(attributes().value(strnumerus) == stryes); + msg.setUtf8(attributes().value(strutf8) == strtrue + || attributes().value(strencoding) == strUtf8); + while (!atEnd()) { + readNext(); + if (isEndElement()) { + // </message> found, finish local loop + msg.setReferences(refs); + translator.append(msg); + break; + } else if (isWhiteSpace()) { + // ignore these, just whitespace + } else if (elementStarts(strsource)) { + // <source>...</source> + msg.setSourceText(readContents()); + } else if (elementStarts(stroldsource)) { + // <oldsource>...</oldsource> + msg.setOldSourceText(readContents()); + } else if (elementStarts(stroldcomment)) { + // <oldcomment>...</oldcomment> + msg.setOldComment(readContents()); + } else if (elementStarts(strextracomment)) { + // <extracomment>...</extracomment> + msg.setExtraComment(readContents()); + } else if (elementStarts(strtranslatorcomment)) { + // <translatorcomment>...</translatorcomment> + msg.setTranslatorComment(readContents()); + } else if (elementStarts(strlocation)) { + // <location/> + QXmlStreamAttributes atts = attributes(); + QString fileName = atts.value(strfilename).toString(); + if (fileName.isEmpty()) { + fileName = currentMsgFile; + } else { + if (refs.isEmpty()) + currentFile = fileName; + currentMsgFile = fileName; + } + const QString lin = atts.value(strline).toString(); + if (lin.isEmpty()) { + translator.setLocationsType(Translator::RelativeLocations); + refs.append(TranslatorMessage::Reference(fileName, -1)); + } else { + bool bOK; + int lineNo = lin.toInt(&bOK); + if (bOK) { + if (lin.startsWith(QLatin1Char('+')) || lin.startsWith(QLatin1Char('-'))) { + lineNo = (currentLine[fileName] += lineNo); + translator.setLocationsType(Translator::RelativeLocations); + } else { + translator.setLocationsType(Translator::AbsoluteLocations); + } + refs.append(TranslatorMessage::Reference(fileName, lineNo)); + } + } + readContents(); + } else if (elementStarts(strcomment)) { + // <comment>...</comment> + msg.setComment(readContents()); + } else if (elementStarts(struserdata)) { + // <userdata>...</userdata> + msg.setUserData(readContents()); + } else if (elementStarts(strtranslation)) { + // <translation> + QXmlStreamAttributes atts = attributes(); + QStringRef type = atts.value(strtype); + if (type == strunfinished) + msg.setType(TranslatorMessage::Unfinished); + else if (type == strobsolete) + msg.setType(TranslatorMessage::Obsolete); + if (msg.isPlural()) { + QStringList translations; + while (!atEnd()) { + readNext(); + if (isEndElement()) { + break; + } else if (isWhiteSpace()) { + // ignore these, just whitespace + } else if (elementStarts(strnumerusform)) { + translations.append(readTransContents()); + } else { + handleError(); + break; + } + } + msg.setTranslations(translations); + } else { + msg.setTranslation(readTransContents()); + } + // </translation> + } else if (isStartElement() + && name().toString().startsWith(strextrans)) { + // <extra-...> + QString tag = name().toString(); + msg.setExtra(tag.mid(6), readContents()); + // </extra-...> + } else { + handleError(); + } + } + // </message> + } else { + handleError(); + } + } + // </context> + } else { + handleError(); + } + } // </TS> + } else { + handleError(); + } + } + if (hasError()) { + m_cd.appendError(errorString()); + return false; + } + return true; +} + +static QString numericEntity(int ch) +{ + return QString(ch <= 0x20 ? QLatin1String("<byte value=\"x%1\"/>") + : QLatin1String("&#x%1;")) .arg(ch, 0, 16); +} + +static QString protect(const QString &str) +{ + QString result; + result.reserve(str.length() * 12 / 10); + for (int i = 0; i != str.size(); ++i) { + uint c = str.at(i).unicode(); + switch (c) { + case '\"': + result += QLatin1String("""); + break; + case '&': + result += QLatin1String("&"); + break; + case '>': + result += QLatin1String(">"); + break; + case '<': + result += QLatin1String("<"); + break; + case '\'': + result += QLatin1String("'"); + break; + default: + if (c < 0x20 && c != '\r' && c != '\n' && c != '\t') + result += numericEntity(c); + else // this also covers surrogates + result += QChar(c); + } + } + return result; +} + +static QString evilBytes(const QString& str, + bool isUtf8, int format, const QByteArray &codecName) +{ + //qDebug() << "EVIL: " << str << isUtf8 << format << codecName; + if (isUtf8) + return protect(str); + if (format == 20) + return protect(str); + if (codecName == "UTF-8") + return protect(str); + QTextCodec *codec = QTextCodec::codecForName(codecName); + if (!codec) + return protect(str); + QString t = QString::fromLatin1(codec->fromUnicode(protect(str)).data()); + int len = (int) t.length(); + QString result; + // FIXME: Factor is sensible only for latin scripts, probably. + result.reserve(t.length() * 2); + for (int k = 0; k < len; k++) { + if (t[k].unicode() >= 0x7f) + result += numericEntity(t[k].unicode()); + else + result += t[k]; + } + return result; +} + +static void writeExtras(QTextStream &t, const char *indent, + const TranslatorMessage::ExtraData &extras, const QRegExp &drops) +{ + for (Translator::ExtraData::ConstIterator it = extras.begin(); it != extras.end(); ++it) { + if (!drops.exactMatch(it.key())) { + t << indent << "<extra-" << it.key() << '>' + << protect(it.value()) + << "</extra-" << it.key() << ">\n"; + } + } +} + +static void writeVariants(QTextStream &t, const char *indent, const QString &input) +{ + int offset; + if ((offset = input.indexOf(QChar(Translator::DefaultVariantSeparator))) >= 0) { + t << " variants=\"yes\">"; + int start = 0; + forever { + t << "\n " << indent << "<lengthvariant>" + << protect(input.mid(start, offset - start)) + << "</lengthvariant>"; + if (offset == input.length()) + break; + start = offset + 1; + offset = input.indexOf(QChar(Translator::DefaultVariantSeparator), start); + if (offset < 0) + offset = input.length(); + } + t << "\n" << indent; + } else { + t << ">" << protect(input); + } +} + +bool saveTS(const Translator &translator, QIODevice &dev, ConversionData &cd, int format) +{ + bool result = true; + QTextStream t(&dev); + t.setCodec(QTextCodec::codecForName("UTF-8")); + bool trIsUtf8 = (translator.codecName() == "UTF-8"); + //qDebug() << translator.codecName(); + bool fileIsUtf8 = (format == 20 || trIsUtf8); + + // The xml prolog allows processors to easily detect the correct encoding + t << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE TS>\n"; + + if (format == 11) + t << "<TS version=\"1.1\""; + else + t << "<TS version=\"2.0\""; + + QString languageCode = translator.languageCode(); + if (!languageCode.isEmpty() && languageCode != QLatin1String("C")) + t << " language=\"" << languageCode << "\""; + if (format == 20) { + languageCode = translator.sourceLanguageCode(); + if (!languageCode.isEmpty() && languageCode != QLatin1String("C")) + t << " sourcelanguage=\"" << languageCode << "\""; + } + t << ">\n"; + + QByteArray codecName = translator.codecName(); + if (codecName != "ISO-8859-1") + t << "<defaultcodec>" << codecName << "</defaultcodec>\n"; + + QRegExp drops(cd.dropTags().join(QLatin1String("|"))); + + if (format == 20) + writeExtras(t, " ", translator.extras(), drops); + + QHash<QString, QList<TranslatorMessage> > messageOrder; + QList<QString> contextOrder; + foreach (const TranslatorMessage &msg, translator.messages()) { + // no need for such noise + if (msg.type() == TranslatorMessage::Obsolete && msg.translation().isEmpty()) + continue; + + QList<TranslatorMessage> &context = messageOrder[msg.context()]; + if (context.isEmpty()) + contextOrder.append(msg.context()); + context.append(msg); + } + if (cd.sortContexts()) + qSort(contextOrder); + + QHash<QString, int> currentLine; + QString currentFile; + foreach (const QString &context, contextOrder) { + const TranslatorMessage &firstMsg = messageOrder[context].first(); + t << "<context" << ((!fileIsUtf8 && firstMsg.isUtf8()) ? " encoding=\"UTF-8\"" : "") << ">\n"; + + t << " <name>" + << evilBytes(context, firstMsg.isUtf8() || fileIsUtf8, format, codecName) + << "</name>\n"; + foreach (const TranslatorMessage &msg, messageOrder[context]) { + //msg.dump(); + + t << " <message"; + if (!msg.id().isEmpty()) + t << " id=\"" << msg.id() << "\""; + if (format == 11 && !trIsUtf8 && msg.isUtf8()) + t << " encoding=\"UTF-8\""; + if (format == 20 && !trIsUtf8 && msg.isUtf8()) + t << " utf8=\"true\""; + if (msg.isPlural()) + t << " numerus=\"yes\""; + t << ">\n"; + if (translator.locationsType() != Translator::NoLocations) { + QString cfile = currentFile; + bool first = true; + foreach (const TranslatorMessage::Reference &ref, msg.allReferences()) { + QString fn = cd.m_targetDir.relativeFilePath(ref.fileName()) + .replace(QLatin1Char('\\'),QLatin1Char('/')); + int ln = ref.lineNumber(); + QString ld; + if (translator.locationsType() == Translator::RelativeLocations) { + if (ln != -1) { + int dlt = ln - currentLine[fn]; + if (dlt >= 0) + ld.append(QLatin1Char('+')); + ld.append(QString::number(dlt)); + currentLine[fn] = ln; + } + + if (fn != cfile) { + if (first) + currentFile = fn; + cfile = fn; + } else { + fn.clear(); + } + first = false; + } else { + if (ln != -1) + ld = QString::number(ln); + } + t << " <location"; + if (!fn.isEmpty()) + t << " filename=\"" << fn << "\""; + if (!ld.isEmpty()) + t << " line=\"" << ld << "\""; + t << "/>\n"; + } + } + + t << " <source>" + << evilBytes(msg.sourceText(), msg.isUtf8(), format, codecName) + << "</source>\n"; + + if (format != 11 && !msg.oldSourceText().isEmpty()) + t << " <oldsource>" << protect(msg.oldSourceText()) << "</oldsource>\n"; + + if (!msg.comment().isEmpty()) { + t << " <comment>" + << evilBytes(msg.comment(), msg.isUtf8(), format, codecName) + << "</comment>\n"; + } + + if (format != 11) { + + if (!msg.oldComment().isEmpty()) + t << " <oldcomment>" << protect(msg.oldComment()) << "</oldcomment>\n"; + + if (!msg.extraComment().isEmpty()) + t << " <extracomment>" << protect(msg.extraComment()) + << "</extracomment>\n"; + + if (!msg.translatorComment().isEmpty()) + t << " <translatorcomment>" << protect(msg.translatorComment()) + << "</translatorcomment>\n"; + + } + + t << " <translation"; + if (msg.type() == TranslatorMessage::Unfinished) + t << " type=\"unfinished\""; + else if (msg.type() == TranslatorMessage::Obsolete) + t << " type=\"obsolete\""; + if (msg.isPlural()) { + t << ">"; + QStringList translns = translator.normalizedTranslations(msg, cd, &result); + for (int j = 0; j < qMax(1, translns.count()); ++j) { + t << "\n <numerusform"; + writeVariants(t, " ", translns[j]); + t << "</numerusform>"; + } + t << "\n "; + } else { + writeVariants(t, " ", msg.translation()); + } + t << "</translation>\n"; + + if (format != 11) + writeExtras(t, " ", msg.extras(), drops); + + if (!msg.userData().isEmpty()) + t << " <userdata>" << msg.userData() << "</userdata>\n"; + t << " </message>\n"; + } + t << "</context>\n"; + } + + t << "</TS>\n"; + return result; +} + +bool loadTS(Translator &translator, QIODevice &dev, ConversionData &cd) +{ + translator.setLocationsType(Translator::NoLocations); + TSReader reader(dev, cd); + return reader.read(translator); +} + +bool saveTS11(const Translator &translator, QIODevice &dev, ConversionData &cd) +{ + return saveTS(translator, dev, cd, 11); +} + +bool saveTS20(const Translator &translator, QIODevice &dev, ConversionData &cd) +{ + return saveTS(translator, dev, cd, 20); +} + +int initTS() +{ + Translator::FileFormat format; + + format.extension = QLatin1String("ts11"); + format.fileType = Translator::FileFormat::TranslationSource; + format.priority = -1; + format.description = QObject::tr("Qt translation sources (format 1.1)"); + format.loader = &loadTS; + format.saver = &saveTS11; + Translator::registerFileFormat(format); + + format.extension = QLatin1String("ts20"); + format.fileType = Translator::FileFormat::TranslationSource; + format.priority = -1; + format.description = QObject::tr("Qt translation sources (format 2.0)"); + format.loader = &loadTS; + format.saver = &saveTS20; + Translator::registerFileFormat(format); + + // "ts" is always the latest. right now it's ts20. + format.extension = QLatin1String("ts"); + format.fileType = Translator::FileFormat::TranslationSource; + format.priority = 0; + format.description = QObject::tr("Qt translation sources (latest format)"); + format.loader = &loadTS; + format.saver = &saveTS20; + Translator::registerFileFormat(format); + + return 1; +} + +Q_CONSTRUCTOR_FUNCTION(initTS) + +QT_END_NAMESPACE diff --git a/tools/linguist/shared/ts.dtd b/tools/linguist/shared/ts.dtd new file mode 100644 index 0000000..ab77f64 --- /dev/null +++ b/tools/linguist/shared/ts.dtd @@ -0,0 +1,113 @@ +<!-- + ! + ! Some notes to the DTD: + ! + ! The location element is set as optional since it was introduced first in Qt 4.2. + ! The userdata element is set as optional since it was introduced first in Qt 4.4. + ! The source and translation elements are optional starting with version 3.0 + ! (Qt 4.6) to support S60 blank messages. + ! + --> +<!-- + ! Macro used in order to escape byte entities not allowed in an xml document + ! for instance, only #x9, #xA and #xD are allowed characters below #x20. + --> +<!ENTITY % evilstring '(#PCDATA | byte)*' > +<!ELEMENT byte EMPTY> +<!-- value contains decimal (e.g. 1000) or hex (e.g. x3e8) unicode encoding of one char --> +<!ATTLIST byte + value CDATA #REQUIRED> +<!-- + ! This element wildcard is no valid DTD. No better solution available. + ! extra elements may appear in TS and message elements. Each element may appear + ! only once within each scope. The contents are preserved verbatim; any + ! attributes are dropped. Currently recognized extra tags include: + ! extra-po-msgid_plural, extra-po-old_msgid_plural + ! extra-po-flags (comma-space separated list) + ! extra-loc-layout_id + ! extra-loc-feature + ! extra-loc-blank + --> +<!ELEMENT extra-* %evilstring; > +<!ELEMENT TS (defaultcodec?, extra-**, (context|message)+) > +<!ATTLIST TS + version CDATA #IMPLIED + sourcelanguage CDATA #IMPLIED + language CDATA #IMPLIED> +<!-- The encoding to use in the .qm file by default. Default is ISO-8859-1. --> +<!ELEMENT defaultcodec (#PCDATA) > +<!ELEMENT context (name?, comment?, (context|message)+) > +<!ATTLIST context + encoding CDATA #IMPLIED> +<!ELEMENT name %evilstring; > +<!-- If "no", then the context nesting is for informational puposes only --> +<!ATTLIST name + nest (yes|no) "yes"> +<!-- This is "disambiguation" in the (new) API, or "msgctxt" in gettext speak --> +<!ELEMENT comment %evilstring; > +<!-- Previous content of comment (result of merge) --> +<!ELEMENT oldcomment %evilstring; > +<!-- The real comment (added by developer/designer) --> +<!ELEMENT extracomment %evilstring; > +<!-- Comment added by translator --> +<!ELEMENT translatorcomment %evilstring; > +<!ELEMENT message (location*, source?, oldsource?, comment?, oldcomment?, extracomment?, translatorcomment?, translation?, userdata?, extra-**) > +<!-- + ! If utf8 is true, the defaultcodec is overridden and the message is encoded + ! in UTF-8 in the .qm file. + --> +<!ATTLIST message + id CDATA #IMPLIED + utf8 (true|false) "false" + numerus (yes|no) "no"> +<!ELEMENT location EMPTY> +<!-- + ! If the line is omitted, the location specifies only a file. + ! + ! location supports relative specifications as well. Line numbers are + ! relative (explicitly positive or negative) to the last reference to a + ! given filename; each file starts with current line 0. If the filename + ! is omitted, the "current" one is used. For the 1st location in a message, + ! "current" is the filename used for the 1st location of the previous message. + ! For subsequent locations, it is the filename used for the previous location. + ! A single .ts file has either all absolute or all relative locations. + --> +<!ATTLIST location + filename CDATA #IMPLIED + line CDATA #IMPLIED> +<!ELEMENT source %evilstring;> +<!-- Previous content of source (result of merge) --> +<!ELEMENT oldsource %evilstring;> +<!-- + ! The following should really say one evilstring macro or several + ! numerusform or lengthvariant elements, but the DTD can't express this. + --> +<!ELEMENT translation (#PCDATA|byte|numerusform|lengthvariant)* > +<!-- + ! If no type is set, the message is "finished". + ! Length variants must be ordered by falling display length. + ! variants may not be yes if the message has numerus yes. + --> +<!ATTLIST translation + type (unfinished|obsolete) #IMPLIED + variants (yes|no) "no"> +<!-- Deprecated. Use extra-* --> +<!ELEMENT userdata (#PCDATA)* > +<!-- + ! The following should really say one evilstring macro or several + ! lengthvariant elements, but the DTD can't express this. + ! Length variants must be ordered by falling display length. + --> +<!ELEMENT numerusform (#PCDATA|byte|lengthvariant)* > +<!ATTLIST numerusform + plurality (nullar|singular|dual|trial|paucal|greaterpaucal|plural|greaterplural) #IMPLIED> + variants (yes|no) "no"> +<!ELEMENT lengthvariant %evilstring; > +<!-- + ! The translation variants have a priority between 1 ("highest") and 9 ("lowest") + ! Typically longer translations get a higher priority. + ! If omitted, the order of appearance of the variants in the .ts files is used. + --> +<!ATTLIST lengthvariant + priority (1|2|3|4|5|6|7|8|9) #IMPLIED> + diff --git a/tools/linguist/shared/xliff.cpp b/tools/linguist/shared/xliff.cpp new file mode 100644 index 0000000..6acf67a --- /dev/null +++ b/tools/linguist/shared/xliff.cpp @@ -0,0 +1,828 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +** Contact: Qt Software Information (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the either Technology Preview License Agreement or the +** Beta Release License Agreement. +** +** GNU Lesser General Public License Usage +** Alternatively, 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.0, 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at qt-sales@nokia.com. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "translator.h" + +#include <QtCore/QDebug> +#include <QtCore/QMap> +#include <QtCore/QStack> +#include <QtCore/QString> +#include <QtCore/QTextCodec> +#include <QtCore/QTextStream> + +#include <QtXml/QXmlAttributes> +#include <QtXml/QXmlDefaultHandler> +#include <QtXml/QXmlParseException> + + +QT_BEGIN_NAMESPACE + +/** + * Implementation of XLIFF file format for Linguist + */ +//static const char *restypeDomain = "x-gettext-domain"; +static const char *restypeContext = "x-trolltech-linguist-context"; +static const char *restypePlurals = "x-gettext-plurals"; +static const char *restypeDummy = "x-dummy"; +static const char *dataTypeUIFile = "x-trolltech-designer-ui"; +static const char *contextMsgctxt = "x-gettext-msgctxt"; // XXX Troll invention, so far. +static const char *contextOldMsgctxt = "x-gettext-previous-msgctxt"; // XXX Troll invention, so far. +static const char *attribPlural = "trolltech:plural"; +static const char *XLIFF11namespaceURI = "urn:oasis:names:tc:xliff:document:1.1"; +static const char *XLIFF12namespaceURI = "urn:oasis:names:tc:xliff:document:1.2"; +static const char *TrollTsNamespaceURI = "urn:trolltech:names:ts:document:1.0"; + +#define COMBINE4CHARS(c1, c2, c3, c4) \ + (int(c1) << 24 | int(c2) << 16 | int(c3) << 8 | int(c4) ) + +static QString dataType(const TranslatorMessage &m) +{ + QByteArray fileName = m.fileName().toAscii(); + unsigned int extHash = 0; + int pos = fileName.count() - 1; + for (int pass = 0; pass < 4 && pos >=0; ++pass, --pos) { + if (fileName.at(pos) == '.') + break; + extHash |= ((int)fileName.at(pos) << (8*pass)); + } + + switch (extHash) { + case COMBINE4CHARS(0,'c','p','p'): + case COMBINE4CHARS(0,'c','x','x'): + case COMBINE4CHARS(0,'c','+','+'): + case COMBINE4CHARS(0,'h','p','p'): + case COMBINE4CHARS(0,'h','x','x'): + case COMBINE4CHARS(0,'h','+','+'): + return QLatin1String("cpp"); + case COMBINE4CHARS(0, 0 , 0 ,'c'): + case COMBINE4CHARS(0, 0 , 0 ,'h'): + case COMBINE4CHARS(0, 0 ,'c','c'): + case COMBINE4CHARS(0, 0 ,'c','h'): + case COMBINE4CHARS(0, 0 ,'h','h'): + return QLatin1String("c"); + case COMBINE4CHARS(0, 0 ,'u','i'): + return QLatin1String(dataTypeUIFile); //### form? + default: + return QLatin1String("plaintext"); // we give up + } +} + +static void writeIndent(QTextStream &ts, int indent) +{ + ts << QString().fill(QLatin1Char(' '), indent * 2); +} + +struct CharMnemonic +{ + char ch; + char escape; + const char *mnemonic; +}; + +static const CharMnemonic charCodeMnemonics[] = { + {0x07, 'a', "bel"}, + {0x08, 'b', "bs"}, + {0x09, 't', "tab"}, + {0x0a, 'n', "lf"}, + {0x0b, 'v', "vt"}, + {0x0c, 'f', "ff"}, + {0x0d, 'r', "cr"} +}; + +static char charFromEscape(char escape) +{ + for (uint i = 0; i < sizeof(charCodeMnemonics)/sizeof(CharMnemonic); ++i) { + CharMnemonic cm = charCodeMnemonics[i]; + if (cm.escape == escape) + return cm.ch; + } + Q_ASSERT(0); + return escape; +} + +static QString numericEntity(int ch, bool makePhs) +{ + // ### This needs to be reviewed, to reflect the updated XLIFF-PO spec. + if (!makePhs || ch < 7 || ch > 0x0d) + return QString::fromAscii("&#x%1;").arg(QString::number(ch, 16)); + + CharMnemonic cm = charCodeMnemonics[int(ch) - 7]; + QString name = QLatin1String(cm.mnemonic); + char escapechar = cm.escape; + + static int id = 0; + return QString::fromAscii("<ph id=\"ph%1\" ctype=\"x-ch-%2\">\\%3</ph>") + .arg(++id) .arg(name) .arg(escapechar); +} + +static QString protect(const QString &str, bool makePhs = true) +{ + QString result; + int len = str.size(); + for (int i = 0; i != len; ++i) { + uint c = str.at(i).unicode(); + switch (c) { + case '\"': + result += QLatin1String("""); + break; + case '&': + result += QLatin1String("&"); + break; + case '>': + result += QLatin1String(">"); + break; + case '<': + result += QLatin1String("<"); + break; + case '\'': + result += QLatin1String("'"); + break; + default: + if (c < 0x20 && c != '\r' && c != '\n' && c != '\t') + result += numericEntity(c, makePhs); + else // this also covers surrogates + result += QChar(c); + } + } + return result; +} + + +static void writeExtras(QTextStream &ts, int indent, + const TranslatorMessage::ExtraData &extras, const QRegExp &drops) +{ + for (Translator::ExtraData::ConstIterator it = extras.begin(); it != extras.end(); ++it) { + if (!drops.exactMatch(it.key())) { + writeIndent(ts, indent); + ts << "<trolltech:" << it.key() << '>' + << protect(it.value()) + << "</trolltech:" << it.key() << ">\n"; + } + } +} + +static void writeLineNumber(QTextStream &ts, const TranslatorMessage &msg, int indent) +{ + if (msg.lineNumber() == -1) + return; + writeIndent(ts, indent); + ts << "<context-group purpose=\"location\"><context context-type=\"linenumber\">" + << msg.lineNumber() << "</context></context-group>\n"; + foreach (const TranslatorMessage::Reference &ref, msg.extraReferences()) { + writeIndent(ts, indent); + ts << "<context-group purpose=\"location\">"; + if (ref.fileName() != msg.fileName()) + ts << "<context context-type=\"sourcefile\">" << ref.fileName() << "</context>"; + ts << "<context context-type=\"linenumber\">" << ref.lineNumber() + << "</context></context-group>\n"; + } +} + +static void writeComment(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent) +{ + if (!msg.comment().isEmpty()) { + writeIndent(ts, indent); + ts << "<context-group><context context-type=\"" << contextMsgctxt << "\">" + << protect(msg.comment(), false) + << "</context></context-group>\n"; + } + if (!msg.oldComment().isEmpty()) { + writeIndent(ts, indent); + ts << "<context-group><context context-type=\"" << contextOldMsgctxt << "\">" + << protect(msg.oldComment(), false) + << "</context></context-group>\n"; + } + writeExtras(ts, indent, msg.extras(), drops); + if (!msg.extraComment().isEmpty()) { + writeIndent(ts, indent); + ts << "<note annotates=\"source\" from=\"developer\">" + << protect(msg.extraComment()) << "</note>\n"; + } + if (!msg.translatorComment().isEmpty()) { + writeIndent(ts, indent); + ts << "<note from=\"translator\">" + << protect(msg.translatorComment()) << "</note>\n"; + } +} + +static void writeTransUnits(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent, + const Translator &translator, ConversionData &cd, bool *ok) +{ + static int msgid; + QString msgidstr = !msg.id().isEmpty() ? msg.id() : QString::fromAscii("_msg%1").arg(++msgid); + + QStringList translns = translator.normalizedTranslations(msg, cd, ok); + QHash<QString, QString>::const_iterator it; + QString pluralStr; + QStringList sources(msg.sourceText()); + if ((it = msg.extras().find(QString::fromLatin1("po-msgid_plural"))) != msg.extras().end()) + sources.append(*it); + QStringList oldsources; + if (!msg.oldSourceText().isEmpty()) + oldsources.append(msg.oldSourceText()); + if ((it = msg.extras().find(QString::fromLatin1("po-old_msgid_plural"))) != msg.extras().end()) { + if (oldsources.isEmpty()) { + if (sources.count() == 2) + oldsources.append(QString()); + else + pluralStr = QLatin1Char(' ') + QLatin1String(attribPlural) + QLatin1String("=\"yes\""); + } + oldsources.append(*it); + } + + QStringList::const_iterator + srcit = sources.begin(), srcend = sources.end(), + oldsrcit = oldsources.begin(), oldsrcend = oldsources.end(), + transit = translns.begin(), transend = translns.end(); + int plural = 0; + QString source; + while (srcit != srcend || oldsrcit != oldsrcend || transit != transend) { + QByteArray attribs; + QByteArray state; + if (msg.type() == TranslatorMessage::Obsolete) { + if (!msg.isPlural()) + attribs = " translate=\"no\""; + } else if (msg.type() == TranslatorMessage::Finished) { + attribs = " approved=\"yes\""; + } else if (transit != transend && !transit->isEmpty()) { + state = " state=\"needs-review-translation\""; + } + writeIndent(ts, indent); + ts << "<trans-unit id=\"" << msgidstr; + if (msg.isPlural()) + ts << "[" << plural++ << "]"; + ts << "\"" << attribs << ">\n"; + ++indent; + + writeIndent(ts, indent); + if (srcit != srcend) { + source = *srcit; + ++srcit; + } // else just repeat last element + ts << "<source xml:space=\"preserve\">" << protect(source) << "</source>\n"; + + bool puttrans = false; + QString translation; + if (transit != transend) { + translation = *transit; + ++transit; + puttrans = true; + } + do { + if (oldsrcit != oldsrcend && !oldsrcit->isEmpty()) { + writeIndent(ts, indent); + ts << "<alt-trans>\n"; + ++indent; + writeIndent(ts, indent); + ts << "<source xml:space=\"preserve\"" << pluralStr << '>' << protect(*oldsrcit) << "</source>\n"; + if (!puttrans) { + writeIndent(ts, indent); + ts << "<target restype=\"" << restypeDummy << "\"/>\n"; + } + } + + if (puttrans) { + writeIndent(ts, indent); + ts << "<target xml:space=\"preserve\"" << state << ">" << protect(translation) << "</target>\n"; + } + + if (oldsrcit != oldsrcend) { + if (!oldsrcit->isEmpty()) { + --indent; + writeIndent(ts, indent); + ts << "</alt-trans>\n"; + } + ++oldsrcit; + } + + puttrans = false; + } while (srcit == srcend && oldsrcit != oldsrcend); + + if (!msg.isPlural()) { + writeLineNumber(ts, msg, indent); + writeComment(ts, msg, drops, indent); + } + + --indent; + writeIndent(ts, indent); + ts << "</trans-unit>\n"; + } +} + +static void writeMessage(QTextStream &ts, const TranslatorMessage &msg, const QRegExp &drops, int indent, + const Translator &translator, ConversionData &cd, bool *ok) +{ + if (msg.isPlural()) { + writeIndent(ts, indent); + ts << "<group restype=\"" << restypePlurals << "\""; + if (!msg.id().isEmpty()) + ts << " id=\"" << msg.id() << "\""; + if (msg.type() == TranslatorMessage::Obsolete) + ts << " translate=\"no\""; + ts << ">\n"; + ++indent; + writeLineNumber(ts, msg, indent); + writeComment(ts, msg, drops, indent); + + writeTransUnits(ts, msg, drops, indent, translator, cd, ok); + --indent; + writeIndent(ts, indent); + ts << "</group>\n"; + } else { + writeTransUnits(ts, msg, drops, indent, translator, cd, ok); + } +} + + +class XLIFFHandler : public QXmlDefaultHandler +{ +public: + XLIFFHandler(Translator &translator, ConversionData &cd); + + bool startElement(const QString& namespaceURI, const QString &localName, + const QString &qName, const QXmlAttributes &atts ); + bool endElement(const QString& namespaceURI, const QString &localName, + const QString &qName ); + bool characters(const QString &ch); + bool fatalError(const QXmlParseException &exception); + + bool endDocument(); + +private: + enum XliffContext { + XC_xliff, + XC_group, + XC_trans_unit, + XC_context_group, + XC_context_group_any, + XC_context, + XC_context_filename, + XC_context_linenumber, + XC_context_context, + XC_context_comment, + XC_context_old_comment, + XC_ph, + XC_extra_comment, + XC_translator_comment, + XC_restype_context, + XC_restype_translation, + XC_restype_plurals, + XC_alt_trans + }; + void pushContext(XliffContext ctx); + bool popContext(XliffContext ctx); + XliffContext currentContext() const; + bool hasContext(XliffContext ctx) const; + bool finalizeMessage(bool isPlural); + +private: + Translator &m_translator; + ConversionData &m_cd; + TranslatorMessage::Type m_type; + QString m_language; + QString m_sourceLanguage; + QString m_context; + QString m_id; + QStringList m_sources; + QStringList m_oldSources; + QString m_comment; + QString m_oldComment; + QString m_extraComment; + QString m_translatorComment; + bool m_isPlural; + bool m_hadAlt; + QStringList m_translations; + QString m_fileName; + int m_lineNumber; + QString m_extraFileName; + TranslatorMessage::References m_refs; + TranslatorMessage::ExtraData m_extra; + + QString accum; + QString m_ctype; + const QString m_URITT; // convenience and efficiency + const QString m_URI; // ... + const QString m_URI12; // ... + QStack<int> m_contextStack; +}; + +XLIFFHandler::XLIFFHandler(Translator &translator, ConversionData &cd) + : m_translator(translator), m_cd(cd), + m_type(TranslatorMessage::Finished), + m_lineNumber(-1), + m_URITT(QLatin1String(TrollTsNamespaceURI)), + m_URI(QLatin1String(XLIFF11namespaceURI)), + m_URI12(QLatin1String(XLIFF12namespaceURI)) +{} + + +void XLIFFHandler::pushContext(XliffContext ctx) +{ + m_contextStack.push_back(ctx); +} + +// Only pops it off if the top of the stack contains ctx +bool XLIFFHandler::popContext(XliffContext ctx) +{ + if (!m_contextStack.isEmpty() && m_contextStack.top() == ctx) { + m_contextStack.pop(); + return true; + } + return false; +} + +XLIFFHandler::XliffContext XLIFFHandler::currentContext() const +{ + if (!m_contextStack.isEmpty()) + return (XliffContext)m_contextStack.top(); + return XC_xliff; +} + +// traverses to the top to check all of the parent contexes. +bool XLIFFHandler::hasContext(XliffContext ctx) const +{ + for (int i = m_contextStack.count() - 1; i >= 0; --i) { + if (m_contextStack.at(i) == ctx) + return true; + } + return false; +} + +bool XLIFFHandler::startElement(const QString& namespaceURI, + const QString &localName, const QString &qName, const QXmlAttributes &atts ) +{ + Q_UNUSED(qName); + if (namespaceURI == m_URITT) + goto bail; + if (namespaceURI != m_URI && namespaceURI != m_URI12) + return false; + if (localName == QLatin1String("xliff")) { + // make sure that the stack is not empty during parsing + pushContext(XC_xliff); + } else if (localName == QLatin1String("file")) { + m_fileName = atts.value(QLatin1String("original")); + m_language = atts.value(QLatin1String("target-language")); + m_sourceLanguage = atts.value(QLatin1String("source-language")); + } else if (localName == QLatin1String("group")) { + if (atts.value(QLatin1String("restype")) == QLatin1String(restypeContext)) { + m_context = atts.value(QLatin1String("resname")); + pushContext(XC_restype_context); + } else { + if (atts.value(QLatin1String("restype")) == QLatin1String(restypePlurals)) { + pushContext(XC_restype_plurals); + m_id = atts.value(QLatin1String("id")); + if (atts.value(QLatin1String("translate")) == QLatin1String("no")) + m_type = TranslatorMessage::Obsolete; + } else { + pushContext(XC_group); + } + } + } else if (localName == QLatin1String("trans-unit")) { + if (!hasContext(XC_restype_plurals) || m_sources.isEmpty() /* who knows ... */) + if (atts.value(QLatin1String("translate")) == QLatin1String("no")) + m_type = TranslatorMessage::Obsolete; + if (!hasContext(XC_restype_plurals)) { + m_id = atts.value(QLatin1String("id")); + if (m_id.startsWith(QLatin1String("_msg"))) + m_id.clear(); + } + if (m_type != TranslatorMessage::Obsolete && + atts.value(QLatin1String("approved")) != QLatin1String("yes")) + m_type = TranslatorMessage::Unfinished; + pushContext(XC_trans_unit); + m_hadAlt = false; + } else if (localName == QLatin1String("alt-trans")) { + pushContext(XC_alt_trans); + } else if (localName == QLatin1String("source")) { + m_isPlural = atts.value(QLatin1String(attribPlural)) == QLatin1String("yes"); + } else if (localName == QLatin1String("target")) { + if (atts.value(QLatin1String("restype")) != QLatin1String(restypeDummy)) + pushContext(XC_restype_translation); + } else if (localName == QLatin1String("context-group")) { + QString purpose = atts.value(QLatin1String("purpose")); + if (purpose == QLatin1String("location")) + pushContext(XC_context_group); + else + pushContext(XC_context_group_any); + } else if (currentContext() == XC_context_group && localName == QLatin1String("context")) { + QString ctxtype = atts.value(QLatin1String("context-type")); + if (ctxtype == QLatin1String("linenumber")) + pushContext(XC_context_linenumber); + else if (ctxtype == QLatin1String("sourcefile")) + pushContext(XC_context_filename); + } else if (currentContext() == XC_context_group_any && localName == QLatin1String("context")) { + QString ctxtype = atts.value(QLatin1String("context-type")); + if (ctxtype == QLatin1String(contextMsgctxt)) + pushContext(XC_context_comment); + else if (ctxtype == QLatin1String(contextOldMsgctxt)) + pushContext(XC_context_old_comment); + } else if (localName == QLatin1String("note")) { + if (atts.value(QLatin1String("annotates")) == QLatin1String("source") && + atts.value(QLatin1String("from")) == QLatin1String("developer")) + pushContext(XC_extra_comment); + else + pushContext(XC_translator_comment); + } else if (localName == QLatin1String("ph")) { + QString ctype = atts.value(QLatin1String("ctype")); + if (ctype.startsWith(QLatin1String("x-ch-"))) + m_ctype = ctype.mid(5); + pushContext(XC_ph); + } +bail: + if (currentContext() != XC_ph) + accum.clear(); + return true; +} + +bool XLIFFHandler::endElement(const QString &namespaceURI, const QString& localName, + const QString &qName) +{ + Q_UNUSED(qName); + if (namespaceURI == m_URITT) { + if (hasContext(XC_trans_unit) || hasContext(XC_restype_plurals)) + m_extra[localName] = accum; + else + m_translator.setExtra(localName, accum); + return true; + } + if (namespaceURI != m_URI && namespaceURI != m_URI12) + return false; + //qDebug() << "URI:" << namespaceURI << "QNAME:" << qName; + if (localName == QLatin1String("xliff")) { + popContext(XC_xliff); + } else if (localName == QLatin1String("source")) { + if (hasContext(XC_alt_trans)) { + if (m_isPlural && m_oldSources.isEmpty()) + m_oldSources.append(QString()); + m_oldSources.append(accum); + m_hadAlt = true; + } else { + m_sources.append(accum); + } + } else if (localName == QLatin1String("target")) { + if (popContext(XC_restype_translation)) + m_translations.append(accum); + } else if (localName == QLatin1String("context-group")) { + if (popContext(XC_context_group)) { + m_refs.append(TranslatorMessage::Reference( + m_extraFileName.isEmpty() ? m_fileName : m_extraFileName, m_lineNumber)); + m_extraFileName.clear(); + m_lineNumber = -1; + } else { + popContext(XC_context_group_any); + } + } else if (localName == QLatin1String("context")) { + if (popContext(XC_context_linenumber)) { + bool ok; + m_lineNumber = accum.trimmed().toInt(&ok); + if (!ok) + m_lineNumber = -1; + } else if (popContext(XC_context_filename)) { + m_extraFileName = accum; + } else if (popContext(XC_context_comment)) { + m_comment = accum; + } else if (popContext(XC_context_old_comment)) { + m_oldComment = accum; + } + } else if (localName == QLatin1String("note")) { + if (popContext(XC_extra_comment)) + m_extraComment = accum; + else if (popContext(XC_translator_comment)) + m_translatorComment = accum; + } else if (localName == QLatin1String("ph")) { + m_ctype.clear(); + popContext(XC_ph); + } else if (localName == QLatin1String("trans-unit")) { + popContext(XC_trans_unit); + if (!m_hadAlt) + m_oldSources.append(QString()); + if (!hasContext(XC_restype_plurals)) { + if (!finalizeMessage(false)) + return false; + } + } else if (localName == QLatin1String("alt-trans")) { + popContext(XC_alt_trans); + } else if (localName == QLatin1String("group")) { + if (popContext(XC_restype_plurals)) { + if (!finalizeMessage(true)) + return false; + } else if (popContext(XC_restype_context)) { + m_context.clear(); + } else { + popContext(XC_group); + } + } + return true; +} + +bool XLIFFHandler::characters(const QString &ch) +{ + if (currentContext() == XC_ph) { + // handle the content of <ph> elements + for (int i = 0; i < ch.count(); ++i) { + QChar chr = ch.at(i); + if (accum.endsWith(QLatin1Char('\\'))) + accum[accum.size() - 1] = QLatin1Char(charFromEscape(chr.toAscii())); + else + accum.append(chr); + } + } else { + QString t = ch; + t.replace(QLatin1String("\r"), QLatin1String("")); + accum.append(t); + } + return true; +} + +bool XLIFFHandler::endDocument() +{ + m_translator.setLanguageCode(m_language); + m_translator.setSourceLanguageCode(m_sourceLanguage); + return true; +} + +bool XLIFFHandler::finalizeMessage(bool isPlural) +{ + if (m_sources.isEmpty()) { + m_cd.appendError(QLatin1String("XLIFF syntax error: Message without source string.")); + return false; + } + TranslatorMessage msg(m_context, m_sources[0], + m_comment, QString(), QString(), -1, + m_translations, m_type, isPlural); + msg.setId(m_id); + msg.setReferences(m_refs); + msg.setOldComment(m_oldComment); + msg.setExtraComment(m_extraComment); + msg.setTranslatorComment(m_translatorComment); + if (m_sources.count() > 1 && m_sources[1] != m_sources[0]) + m_extra.insert(QLatin1String("po-msgid_plural"), m_sources[1]); + if (!m_oldSources.isEmpty()) { + if (!m_oldSources[0].isEmpty()) + msg.setOldSourceText(m_oldSources[0]); + if (m_oldSources.count() > 1 && m_oldSources[1] != m_oldSources[0]) + m_extra.insert(QLatin1String("po-old_msgid_plural"), m_oldSources[1]); + } + msg.setExtras(m_extra); + m_translator.append(msg); + + m_id.clear(); + m_sources.clear(); + m_oldSources.clear(); + m_translations.clear(); + m_comment.clear(); + m_oldComment.clear(); + m_extraComment.clear(); + m_translatorComment.clear(); + m_extra.clear(); + m_refs.clear(); + m_type = TranslatorMessage::Finished; + return true; +} + +bool XLIFFHandler::fatalError(const QXmlParseException &exception) +{ + QString msg; + msg.sprintf("XML error: Parse error at line %d, column %d (%s).\n", + exception.lineNumber(), exception.columnNumber(), + exception.message().toLatin1().data() ); + m_cd.appendError(msg); + return false; +} + +bool loadXLIFF(Translator &translator, QIODevice &dev, ConversionData &cd) +{ + QXmlInputSource in(&dev); + QXmlSimpleReader reader; + XLIFFHandler hand(translator, cd); + reader.setContentHandler(&hand); + reader.setErrorHandler(&hand); + return reader.parse(in); +} + +bool saveXLIFF(const Translator &translator, QIODevice &dev, ConversionData &cd) +{ + bool ok = true; + int indent = 0; + + QTextStream ts(&dev); + ts.setCodec(QTextCodec::codecForName("UTF-8")); + + QStringList dtgs = cd.dropTags(); + dtgs << QLatin1String("po-(old_)?msgid_plural"); + QRegExp drops(dtgs.join(QLatin1String("|"))); + + QHash<QString, QHash<QString, QList<TranslatorMessage> > > messageOrder; + QHash<QString, QList<QString> > contextOrder; + QList<QString> fileOrder; + foreach (const TranslatorMessage &msg, translator.messages()) { + QHash<QString, QList<TranslatorMessage> > &file = messageOrder[msg.fileName()]; + if (file.isEmpty()) + fileOrder.append(msg.fileName()); + QList<TranslatorMessage> &context = file[msg.context()]; + if (context.isEmpty()) + contextOrder[msg.fileName()].append(msg.context()); + context.append(msg); + } + + ts.setFieldAlignment(QTextStream::AlignRight); + ts << "<?xml version=\"1.0\""; + ts << " encoding=\"utf-8\"?>\n"; + ts << "<xliff version=\"1.2\" xmlns=\"" << XLIFF12namespaceURI + << "\" xmlns:trolltech=\"" << TrollTsNamespaceURI << "\">\n"; + ++indent; + writeExtras(ts, indent, translator.extras(), drops); + foreach (const QString &fn, fileOrder) { + writeIndent(ts, indent); + ts << "<file original=\"" << fn << "\"" + << " datatype=\"" << dataType(messageOrder[fn].begin()->first()) << "\"" + << " source-language=\"" + << (translator.sourceLanguageCode().isEmpty() ? + QByteArray("en") : translator.sourceLanguageCode().toLatin1()) << "\"" + << " target-language=\"" << translator.languageCode() << "\"" + << "><body>\n"; + ++indent; + + foreach (const QString &ctx, contextOrder[fn]) { + if (!ctx.isEmpty()) { + writeIndent(ts, indent); + ts << "<group restype=\"" << restypeContext << "\"" + << " resname=\"" << protect(ctx) << "\">\n"; + ++indent; + } + + foreach (const TranslatorMessage &msg, messageOrder[fn][ctx]) + writeMessage(ts, msg, drops, indent, translator, cd, &ok); + + if (!ctx.isEmpty()) { + --indent; + writeIndent(ts, indent); + ts << "</group>\n"; + } + } + + --indent; + writeIndent(ts, indent); + ts << "</body></file>\n"; + } + --indent; + writeIndent(ts, indent); + ts << "</xliff>\n"; + + return ok; +} + +int initXLIFF() +{ + Translator::FileFormat format; + format.extension = QLatin1String("xlf"); + format.description = QObject::tr("XLIFF localization files"); + format.fileType = Translator::FileFormat::TranslationSource; + format.priority = 1; + format.loader = &loadXLIFF; + format.saver = &saveXLIFF; + Translator::registerFileFormat(format); + return 1; +} + +Q_CONSTRUCTOR_FUNCTION(initXLIFF) + +QT_END_NAMESPACE |