diff --git a/src/ktranscript.cpp b/src/ktranscript.cpp index e0bb146..87b43b4 100644 --- a/src/ktranscript.cpp +++ b/src/ktranscript.cpp @@ -1,1618 +1,1617 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Chusslove Illich Copyright (C) 2014 Kevin Krammer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include //#include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include class KTranscriptImp; class Scriptface; typedef QHash TsConfigGroup; typedef QHash TsConfig; // Transcript implementation (used as singleton). class KTranscriptImp : public KTranscript { public: KTranscriptImp(); ~KTranscriptImp() override; QString eval(const QList &argv, const QString &lang, const QString &ctry, const QString &msgctxt, const QHash &dynctxt, const QString &msgid, const QStringList &subs, const QList &vals, const QString &ftrans, QList &mods, QString &error, bool &fallback) override; QStringList postCalls(const QString &lang) override; // Lexical path of the module for the executing code. QString currentModulePath; private: void loadModules(const QList &mods, QString &error); void setupInterpreter(const QString &lang); TsConfig config; QHash m_sface; }; // Script-side transcript interface. class Scriptface : public QObject { Q_OBJECT public: explicit Scriptface(const TsConfigGroup &config, QObject *parent = nullptr); ~Scriptface(); // Interface functions. Q_INVOKABLE QJSValue load(const QString &name); Q_INVOKABLE QJSValue setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval = QJSValue::NullValue); Q_INVOKABLE QJSValue hascall(const QString &name); Q_INVOKABLE QJSValue acallInternal(const QJSValue &args); Q_INVOKABLE QJSValue setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval = QJSValue::NullValue); Q_INVOKABLE QJSValue fallback(); Q_INVOKABLE QJSValue nsubs(); Q_INVOKABLE QJSValue subs(const QJSValue &index); Q_INVOKABLE QJSValue vals(const QJSValue &index); Q_INVOKABLE QJSValue msgctxt(); Q_INVOKABLE QJSValue dynctxt(const QString &key); Q_INVOKABLE QJSValue msgid(); Q_INVOKABLE QJSValue msgkey(); Q_INVOKABLE QJSValue msgstrf(); Q_INVOKABLE void dbgputs(const QString &str); Q_INVOKABLE void warnputs(const QString &str); Q_INVOKABLE QJSValue localeCountry(); Q_INVOKABLE QJSValue normKey(const QJSValue &phrase); Q_INVOKABLE QJSValue loadProps(const QString &name); Q_INVOKABLE QJSValue getProp(const QJSValue &phrase, const QJSValue &prop); Q_INVOKABLE QJSValue setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value); Q_INVOKABLE QJSValue toUpperFirst(const QJSValue &str, const QJSValue &nalt = QJSValue::NullValue); Q_INVOKABLE QJSValue toLowerFirst(const QJSValue &str, const QJSValue &nalt = QJSValue::NullValue); Q_INVOKABLE QJSValue getConfString(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue); Q_INVOKABLE QJSValue getConfBool(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue); Q_INVOKABLE QJSValue getConfNumber(const QJSValue &key, const QJSValue &dval = QJSValue::NullValue); // Helper methods to interface functions. QJSValue load(const QJSValueList &names); QJSValue loadProps(const QJSValueList &names); QString loadProps_text(const QString &fpath); QString loadProps_bin(const QString &fpath); QString loadProps_bin_00(const QString &fpath); QString loadProps_bin_01(const QString &fpath); void put(const QString &propertyName, const QJSValue &value); // Link to its script engine QJSEngine *const scriptEngine; // Current message data. const QString *msgcontext; const QHash *dyncontext; const QString *msgId; const QStringList *subList; const QList *valList; const QString *ftrans; const QString *ctry; // Fallback request handle. bool *fallbackRequest; // Function register. QHash funcs; QHash fvals; QHash fpaths; // Ordering of those functions which execute for all messages. QList nameForalls; // Property values per phrase (used by *Prop interface calls). // Not QStrings, in order to avoid conversion from UTF-8 when // loading compiled maps (less latency on startup). QHash > phraseProps; // Unresolved property values per phrase, // containing the pointer to compiled pmap file handle and offset in it. QHash > phraseUnparsedProps; QHash resolveUnparsedProps(const QByteArray &phrase); // Set of loaded pmap files by paths and file handle pointers. QSet loadedPmapPaths; QSet loadedPmapHandles; // User config. TsConfigGroup config; }; // ---------------------------------------------------------------------- // Custom debug and warning output (kdebug not available) #define DBGP "KTranscript: " void dbgout(const char *str) { #ifndef NDEBUG fprintf(stderr, DBGP"%s\n", str); #else Q_UNUSED(str); #endif } template void dbgout(const char *str, const T1 &a1) { #ifndef NDEBUG fprintf(stderr, DBGP"%s\n", QString::fromUtf8(str).arg(a1).toLocal8Bit().data()); #else Q_UNUSED(str); Q_UNUSED(a1); #endif } template void dbgout(const char *str, const T1 &a1, const T2 &a2) { #ifndef NDEBUG fprintf(stderr, DBGP"%s\n", QString::fromUtf8(str).arg(a1).arg(a2).toLocal8Bit().data()); #else Q_UNUSED(str); Q_UNUSED(a1); Q_UNUSED(a2); #endif } template void dbgout(const char *str, const T1 &a1, const T2 &a2, const T3 &a3) { #ifndef NDEBUG fprintf(stderr, DBGP"%s\n", QString::fromUtf8(str).arg(a1).arg(a2).arg(a3).toLocal8Bit().data()); #else Q_UNUSED(str); Q_UNUSED(a1); Q_UNUSED(a2); Q_UNUSED(a3); #endif } #define WARNP "KTranscript: " void warnout(const char *str) { fprintf(stderr, WARNP"%s\n", str); } template void warnout(const char *str, const T1 &a1) { fprintf(stderr, WARNP"%s\n", QString::fromUtf8(str).arg(a1).toLocal8Bit().data()); } // ---------------------------------------------------------------------- // Produces a string out of a script exception. QString expt2str(const QJSValue &expt) { if (expt.isError()) { const QJSValue message = expt.property(QStringLiteral("message")); if (!message.isUndefined()) { return QStringLiteral("Error: %1").arg(message.toString()); } } QString strexpt = expt.toString(); return QStringLiteral("Caught exception: %1").arg(strexpt); } // ---------------------------------------------------------------------- // Count number of lines in the string, // up to and excluding the requested position. int countLines(const QString &s, int p) { int n = 1; int len = s.length(); for (int i = 0; i < p && i < len; ++i) { if (s[i] == QLatin1Char('\n')) { ++n; } } return n; } // ---------------------------------------------------------------------- // Normalize string key for hash lookups, QByteArray normKeystr(const QString &raw, bool mayHaveAcc = true) { // NOTE: Regexes should not be used here for performance reasons. // This function may potentially be called thousands of times // on application startup. QString key = raw; // Strip all whitespace. int len = key.length(); QString nkey; for (int i = 0; i < len; ++i) { QChar c = key[i]; if (!c.isSpace()) { nkey.append(c); } } key = nkey; // Strip accelerator marker. if (mayHaveAcc) { key = removeAcceleratorMarker(key); } // Convert to lower case. key = key.toLower(); return key.toUtf8(); } // ---------------------------------------------------------------------- // Trim multiline string in a "smart" way: // Remove leading and trailing whitespace up to and including first // newline from that side, if there is one; otherwise, don't touch. QString trimSmart(const QString &raw) { // NOTE: This could be done by a single regex, but is not due to // performance reasons. // This function may potentially be called thousands of times // on application startup. int len = raw.length(); int is = 0; while (is < len && raw[is].isSpace() && raw[is] != QLatin1Char('\n')) { ++is; } if (is >= len || raw[is] != QLatin1Char('\n')) { is = -1; } int ie = len - 1; while (ie >= 0 && raw[ie].isSpace() && raw[ie] != QLatin1Char('\n')) { --ie; } if (ie < 0 || raw[ie] != QLatin1Char('\n')) { ie = len; } return raw.mid(is + 1, ie - is - 1); } // ---------------------------------------------------------------------- // Produce a JavaScript object out of Qt variant. QJSValue variantToJsValue(const QVariant &val) { QVariant::Type vtype = val.type(); if (vtype == QVariant::String) { return QJSValue(val.toString()); } else if (vtype == QVariant::Bool) { return QJSValue(val.toBool()); } else if (vtype == QVariant::Double || vtype == QVariant::Int || vtype == QVariant::UInt || vtype == QVariant::LongLong || vtype == QVariant::ULongLong) { return QJSValue(val.toDouble()); } else { return QJSValue::UndefinedValue; } } // ---------------------------------------------------------------------- // Parse ini-style config file, // returning content as hash of hashes by group and key. // Parsing is not fussy, it will read what it can. TsConfig readConfig(const QString &fname) { TsConfig config; // Add empty group. TsConfig::iterator configGroup; configGroup = config.insert(QString(), TsConfigGroup()); QFile file(fname); if (!file.open(QIODevice::ReadOnly)) { return config; } QTextStream stream(&file); stream.setCodec("UTF-8"); while (!stream.atEnd()) { QString line = stream.readLine(); int p1, p2; // Remove comment from the line. p1 = line.indexOf(QLatin1Char('#')); if (p1 >= 0) { line.truncate(p1); } line = line.trimmed(); if (line.isEmpty()) { continue; } if (line[0] == QLatin1Char('[')) { // Group switch. p1 = 0; p2 = line.indexOf(QLatin1Char(']'), p1 + 1); if (p2 < 0) { continue; } QString group = line.mid(p1 + 1, p2 - p1 - 1).trimmed(); configGroup = config.find(group); if (configGroup == config.end()) { // Add new group. configGroup = config.insert(group, TsConfigGroup()); } } else { // Field. p1 = line.indexOf(QLatin1Char('=')); if (p1 < 0) { continue; } QStringRef field = line.leftRef(p1).trimmed(); QStringRef value = line.midRef(p1 + 1).trimmed(); if (!field.isEmpty()) { (*configGroup)[field.toString()] = value.toString(); } } } file.close(); return config; } // ---------------------------------------------------------------------- // throw or log error, depending on context availability static QJSValue throwError(QJSEngine *engine, const QString &message) { if (engine) { return engine->evaluate(QStringLiteral("new Error(%1)").arg(message)); } qCritical() << "Script error" << message; return QJSValue::UndefinedValue; } #ifdef KTRANSCRIPT_TESTBUILD // ---------------------------------------------------------------------- // Test build creation/destruction hooks static KTranscriptImp *s_transcriptInstance = nullptr; KTranscriptImp *globalKTI() { return s_transcriptInstance; } KTranscript *autotestCreateKTranscriptImp() { Q_ASSERT(s_transcriptInstance == nullptr); s_transcriptInstance = new KTranscriptImp; return s_transcriptInstance; } void autotestDestroyKTranscriptImp() { Q_ASSERT(s_transcriptInstance != nullptr); delete s_transcriptInstance; s_transcriptInstance = nullptr; } #else // ---------------------------------------------------------------------- // Dynamic loading. Q_GLOBAL_STATIC(KTranscriptImp, globalKTI) extern "C" { KTRANSCRIPT_EXPORT KTranscript *load_transcript() { return globalKTI(); } } #endif // ---------------------------------------------------------------------- // KTranscript definitions. KTranscriptImp::KTranscriptImp() { // Load user configuration. QString tsConfigPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("ktranscript.ini")); if (tsConfigPath.isEmpty()) { tsConfigPath = QDir::homePath() + QLatin1Char('/') + QLatin1String(".transcriptrc"); } config = readConfig(tsConfigPath); } KTranscriptImp::~KTranscriptImp() { qDeleteAll(m_sface); } QString KTranscriptImp::eval(const QList &argv, const QString &lang, const QString &ctry, const QString &msgctxt, const QHash &dynctxt, const QString &msgid, const QStringList &subs, const QList &vals, const QString &ftrans, QList &mods, QString &error, bool &fallback) { //error = "debug"; return QString(); error.clear(); // empty error message means successful evaluation fallback = false; // fallback not requested #if 0 // FIXME: Maybe not needed, as QJSEngine has no native outside access? // Unportable (needs unistd.h)? // If effective user id is root and real user id is not root. if (geteuid() == 0 && getuid() != 0) { // Since scripts are user input, and the program is running with // root permissions while real user is not root, do not invoke // scripting at all, to prevent exploits. error = "Security block: trying to execute a script in suid environment."; return QString(); } #endif // Load any new modules and clear the list. if (!mods.isEmpty()) { loadModules(mods, error); mods.clear(); if (!error.isEmpty()) { return QString(); } } // Add interpreters for new languages. // (though it should never happen here, but earlier when loading modules; // this also means there are no calls set, so the unregistered call error // below will be reported). if (!m_sface.contains(lang)) { setupInterpreter(lang); } // Shortcuts. Scriptface *sface = m_sface[lang]; QJSEngine *engine = sface->scriptEngine; QJSValue gobj = engine->globalObject(); // Link current message data for script-side interface. sface->msgcontext = &msgctxt; sface->dyncontext = &dynctxt; sface->msgId = &msgid; sface->subList = &subs; sface->valList = &vals; sface->ftrans = &ftrans; sface->fallbackRequest = &fallback; sface->ctry = &ctry; // Find corresponding JS function. int argc = argv.size(); if (argc < 1) { //error = "At least the call name must be supplied."; // Empty interpolation is OK, possibly used just to initialize // at a given point (e.g. for Ts.setForall() to start having effect). return QString(); } QString funcName = argv[0].toString(); if (!sface->funcs.contains(funcName)) { error = QStringLiteral("Unregistered call to '%1'.").arg(funcName); return QString(); } QJSValue func = sface->funcs[funcName]; QJSValue fval = sface->fvals[funcName]; // Recover module path from the time of definition of this call, // for possible load calls. currentModulePath = sface->fpaths[funcName]; // Execute function. QJSValueList arglist; arglist.reserve(argc-1); for (int i = 1; i < argc; ++i) { arglist.append(engine->toScriptValue(argv[i])); } QJSValue val; if (fval.isObject()) { val = func.callWithInstance(fval, arglist); } else { // no object associated to this function, use global val = func.callWithInstance(gobj, arglist); } if (fallback) { // Fallback to ordinary translation requested. return QString(); } else if (!val.isError()) { // Evaluation successful. if (val.isString()) { // Good to go. return val.toString(); } else { // Accept only strings. QString strval = val.toString(); error = QStringLiteral("Non-string return value: %1").arg(strval); return QString(); } } else { // Exception raised. error = expt2str(val); return QString(); } } QStringList KTranscriptImp::postCalls(const QString &lang) { // Return no calls if scripting was not already set up for this language. // NOTE: This shouldn't happen, as postCalls cannot be called in such case. if (!m_sface.contains(lang)) { return QStringList(); } // Shortcuts. Scriptface *sface = m_sface[lang]; return sface->nameForalls; } void KTranscriptImp::loadModules(const QList &mods, QString &error) { QList modErrors; for (const QStringList &mod : mods) { QString mpath = mod[0]; QString mlang = mod[1]; // Add interpreters for new languages. if (!m_sface.contains(mlang)) { setupInterpreter(mlang); } // Setup current module path for loading submodules. // (sort of closure over invocations of loadf) int posls = mpath.lastIndexOf(QLatin1Char('/')); if (posls < 1) { modErrors.append(QStringLiteral( "Funny module path '%1', skipping.").arg(mpath)); continue; } currentModulePath = mpath.left(posls); QString fname = mpath.mid(posls + 1); // Scriptface::loadf() wants no extension on the filename fname = fname.left(fname.lastIndexOf(QLatin1Char('.'))); // Load the module. QJSValueList alist; alist.append(QJSValue(fname)); m_sface[mlang]->load(alist); } // Unset module path. currentModulePath.clear(); for (const QString &merr : qAsConst(modErrors)) { error.append(merr + QLatin1Char('\n')); } } #define SFNAME "Ts" void KTranscriptImp::setupInterpreter(const QString &lang) { // Add scripting interface // Creates its own script engine and registers with it // NOTE: Config may not contain an entry for the language, in which case // it is automatically constructed as an empty hash. This is intended. Scriptface *sface = new Scriptface(config[lang]); // Store scriptface m_sface[lang] = sface; //dbgout("=====> Created interpreter for '%1'", lang); } Scriptface::Scriptface(const TsConfigGroup &config_, QObject *parent) : QObject(parent), scriptEngine(new QJSEngine), fallbackRequest(nullptr), config(config_) { QJSValue object = scriptEngine->newQObject(this); scriptEngine->globalObject().setProperty(QStringLiteral(SFNAME), object); scriptEngine->evaluate(QStringLiteral("Ts.acall = function() { return Ts.acallInternal(Array.prototype.slice.call(arguments)); };")); } Scriptface::~Scriptface() { qDeleteAll(loadedPmapHandles); scriptEngine->deleteLater(); } void Scriptface::put(const QString &propertyName, const QJSValue &value) { QJSValue internalObject = scriptEngine->globalObject().property(QStringLiteral("ScriptfaceInternal")); if (internalObject.isUndefined()) { internalObject = scriptEngine->newObject(); scriptEngine->globalObject().setProperty(QStringLiteral("ScriptfaceInternal"), internalObject); } internalObject.setProperty(propertyName, value); } // ---------------------------------------------------------------------- // Scriptface interface functions. #ifdef _MSC_VER // Work around bizarre MSVC (2013) bug preventing use of QStringLiteral for concatenated string literals # define SPREF(X) QString::fromLatin1(SFNAME "." X) #else # define SPREF(X) QStringLiteral(SFNAME "." X) #endif QJSValue Scriptface::load(const QString &name) { QJSValueList fnames; fnames << name; return load(fnames); } QJSValue Scriptface::setcall(const QJSValue &name, const QJSValue &func, const QJSValue &fval) { if (!name.isString()) { return throwError(scriptEngine, SPREF("setcall: expected string as first argument")); } if (!func.isCallable()) { return throwError(scriptEngine, SPREF("setcall: expected function as second argument")); } if (!(fval.isObject() || fval.isNull())) { return throwError(scriptEngine, SPREF("setcall: expected object or null as third argument")); } QString qname = name.toString(); funcs[qname] = func; fvals[qname] = fval; // Register values to keep GC from collecting them. Is this needed? put(QStringLiteral("#:f<%1>").arg(qname), func); put(QStringLiteral("#:o<%1>").arg(qname), fval); // Set current module path as module path for this call, // in case it contains load subcalls. fpaths[qname] = globalKTI()->currentModulePath; return QJSValue::UndefinedValue; } QJSValue Scriptface::hascall(const QString &qname) { return QJSValue(funcs.contains(qname)); } QJSValue Scriptface::acallInternal(const QJSValue &args) { QJSValueIterator it(args); if (!it.next()) { return throwError(scriptEngine, SPREF("acall: expected at least one argument (call name)")); } if (!it.value().isString()) { return throwError(scriptEngine, SPREF("acall: expected string as first argument (call name)")); } // Get the function and its context object. QString callname = it.value().toString(); if (!funcs.contains(callname)) { return throwError(scriptEngine, SPREF("acall: unregistered call to '%1'").arg(callname)); } QJSValue func = funcs[callname]; QJSValue fval = fvals[callname]; // Recover module path from the time of definition of this call, // for possible load calls. globalKTI()->currentModulePath = fpaths[callname]; // Execute function. QJSValueList arglist; while (it.next()) arglist.append(it.value()); QJSValue val; if (fval.isObject()) { // Call function with the context object. val = func.callWithInstance(fval, arglist); } else { // No context object associated to this function, use global. val = func.callWithInstance(scriptEngine->globalObject(), arglist); } return val; } QJSValue Scriptface::setcallForall(const QJSValue &name, const QJSValue &func, const QJSValue &fval) { if (!name.isString()) { return throwError(scriptEngine, SPREF("setcallForall: expected string as first argument")); } if (!func.isCallable()) { return throwError(scriptEngine, SPREF("setcallForall: expected function as second argument")); } if (!(fval.isObject() || fval.isNull())) { return throwError(scriptEngine, SPREF("setcallForall: expected object or null as third argument")); } QString qname = name.toString(); funcs[qname] = func; fvals[qname] = fval; // Register values to keep GC from collecting them. Is this needed? put(QStringLiteral("#:fall<%1>").arg(qname), func); put(QStringLiteral("#:oall<%1>").arg(qname), fval); // Set current module path as module path for this call, // in case it contains load subcalls. fpaths[qname] = globalKTI()->currentModulePath; // Put in the queue order for execution on all messages. nameForalls.append(qname); return QJSValue::UndefinedValue; } QJSValue Scriptface::fallback() { if (fallbackRequest) { *fallbackRequest = true; } return QJSValue::UndefinedValue; } QJSValue Scriptface::nsubs() { return QJSValue(subList->size()); } QJSValue Scriptface::subs(const QJSValue &index) { if (!index.isNumber()) { return throwError(scriptEngine, SPREF("subs: expected number as first argument")); } int i = qRound(index.toNumber()); if (i < 0 || i >= subList->size()) { return throwError(scriptEngine, SPREF("subs: index out of range")); } return QJSValue(subList->at(i)); } QJSValue Scriptface::vals(const QJSValue &index) { if (!index.isNumber()) { return throwError(scriptEngine, SPREF("vals: expected number as first argument")); } int i = qRound(index.toNumber()); if (i < 0 || i >= valList->size()) { return throwError(scriptEngine, SPREF("vals: index out of range")); } return scriptEngine->toScriptValue(valList->at(i)); // return variantToJsValue(valList->at(i)); } QJSValue Scriptface::msgctxt() { return QJSValue(*msgcontext); } QJSValue Scriptface::dynctxt(const QString &qkey) { if (dyncontext->contains(qkey)) { return QJSValue(dyncontext->value(qkey)); } return QJSValue::UndefinedValue; } QJSValue Scriptface::msgid() { return QJSValue(*msgId); } QJSValue Scriptface::msgkey() { return QJSValue(QString(*msgcontext + QLatin1Char('|') + *msgId)); } QJSValue Scriptface::msgstrf() { return QJSValue(*ftrans); } void Scriptface::dbgputs(const QString &qstr) { dbgout("[JS-debug] %1", qstr); } void Scriptface::warnputs(const QString &qstr) { warnout("[JS-warning] %1", qstr); } QJSValue Scriptface::localeCountry() { return QJSValue(*ctry); } QJSValue Scriptface::normKey(const QJSValue &phrase) { if (!phrase.isString()) { return throwError(scriptEngine, SPREF("normKey: expected string as argument")); } QByteArray nqphrase = normKeystr(phrase.toString()); return QJSValue(QString::fromUtf8(nqphrase)); } QJSValue Scriptface::loadProps(const QString &name) { QJSValueList fnames; fnames << name; return loadProps(fnames); } QJSValue Scriptface::loadProps(const QJSValueList &fnames) { if (globalKTI()->currentModulePath.isEmpty()) { return throwError(scriptEngine, SPREF("loadProps: no current module path, aiiie...")); } for (int i = 0; i < fnames.size(); ++i) { if (!fnames[i].isString()) { return throwError(scriptEngine, SPREF("loadProps: expected string as file name")); } } for (int i = 0; i < fnames.size(); ++i) { QString qfname = fnames[i].toString(); QString qfpath_base = globalKTI()->currentModulePath + QLatin1Char('/') + qfname; // Determine which kind of map is available. // Give preference to compiled map. QString qfpath = qfpath_base + QLatin1String(".pmapc"); bool haveCompiled = true; QFile file_check(qfpath); if (!file_check.open(QIODevice::ReadOnly)) { haveCompiled = false; qfpath = qfpath_base + QLatin1String(".pmap"); QFile file_check(qfpath); if (!file_check.open(QIODevice::ReadOnly)) { return throwError(scriptEngine, SPREF("loadProps: cannot read map '%1'") .arg(qfpath)); } } file_check.close(); // Load from appropriate type of map. if (!loadedPmapPaths.contains(qfpath)) { QString errorString; if (haveCompiled) { errorString = loadProps_bin(qfpath); } else { errorString = loadProps_text(qfpath); } if (!errorString.isEmpty()) { return throwError(scriptEngine, errorString); } dbgout("Loaded property map: %1", qfpath); loadedPmapPaths.insert(qfpath); } } return QJSValue::UndefinedValue; } QJSValue Scriptface::getProp(const QJSValue &phrase, const QJSValue &prop) { if (!phrase.isString()) { return throwError(scriptEngine, SPREF("getProp: expected string as first argument")); } if (!prop.isString()) { return throwError(scriptEngine, SPREF("getProp: expected string as second argument")); } QByteArray qphrase = normKeystr(phrase.toString()); QHash props = phraseProps.value(qphrase); if (props.isEmpty()) { props = resolveUnparsedProps(qphrase); } if (!props.isEmpty()) { QByteArray qprop = normKeystr(prop.toString()); QByteArray qval = props.value(qprop); if (!qval.isEmpty()) { return QJSValue(QString::fromUtf8(qval)); } } return QJSValue::UndefinedValue; } QJSValue Scriptface::setProp(const QJSValue &phrase, const QJSValue &prop, const QJSValue &value) { if (!phrase.isString()) { return throwError(scriptEngine, SPREF("setProp: expected string as first argument")); } if (!prop.isString()) { return throwError(scriptEngine, SPREF("setProp: expected string as second argument")); } if (!value.isString()) { return throwError(scriptEngine, SPREF("setProp: expected string as third argument")); } QByteArray qphrase = normKeystr(phrase.toString()); QByteArray qprop = normKeystr(prop.toString()); QByteArray qvalue = value.toString().toUtf8(); // Any non-existent key in first or second-level hash will be created. phraseProps[qphrase][qprop] = qvalue; return QJSValue::UndefinedValue; } static QString toCaseFirst(const QString &qstr, int qnalt, bool toupper) { static const QLatin1String head("~@"); static const int hlen = 2; //head.length() // If the first letter is found within an alternatives directive, // change case of the first letter in each of the alternatives. QString qstrcc = qstr; int len = qstr.length(); QChar altSep; int remainingAlts = 0; bool checkCase = true; int numChcased = 0; int i = 0; while (i < len) { QChar c = qstr[i]; if (qnalt && !remainingAlts && qstr.midRef(i, hlen) == head) { // An alternatives directive is just starting. i += 2; if (i >= len) { break; // malformed directive, bail out } // Record alternatives separator, set number of remaining // alternatives, reactivate case checking. altSep = qstrcc[i]; remainingAlts = qnalt; checkCase = true; } else if (remainingAlts && c == altSep) { // Alternative separator found, reduce number of remaining // alternatives and reactivate case checking. --remainingAlts; checkCase = true; } else if (checkCase && c.isLetter()) { // Case check is active and the character is a letter; change case. if (toupper) { qstrcc[i] = c.toUpper(); } else { qstrcc[i] = c.toLower(); } ++numChcased; // No more case checks until next alternatives separator. checkCase = false; } // If any letter has been changed, and there are no more alternatives // to be processed, we're done. if (numChcased > 0 && remainingAlts == 0) { break; } // Go to next character. ++i; } return qstrcc; } QJSValue Scriptface::toUpperFirst(const QJSValue &str, const QJSValue &nalt) { if (!str.isString()) { return throwError(scriptEngine, SPREF("toUpperFirst: expected string as first argument")); } if (!(nalt.isNumber() || nalt.isNull())) { return throwError(scriptEngine, SPREF("toUpperFirst: expected number as second argument")); } QString qstr = str.toString(); int qnalt = nalt.isNull() ? 0 : nalt.toInt(); QString qstruc = toCaseFirst(qstr, qnalt, true); return QJSValue(qstruc); } QJSValue Scriptface::toLowerFirst(const QJSValue &str, const QJSValue &nalt) { if (!str.isString()) { return throwError(scriptEngine, SPREF("toLowerFirst: expected string as first argument")); } if (!(nalt.isNumber() || nalt.isNull())) { return throwError(scriptEngine, SPREF("toLowerFirst: expected number as second argument")); } QString qstr = str.toString(); int qnalt = nalt.isNull() ? 0 : nalt.toInt(); QString qstrlc = toCaseFirst(qstr, qnalt, false); return QJSValue(qstrlc); } QJSValue Scriptface::getConfString(const QJSValue &key, const QJSValue &dval) { if (!key.isString()) { return throwError(scriptEngine, QStringLiteral("getConfString: expected string as first argument")); } if (!(dval.isString() || dval.isNull())) { return throwError(scriptEngine, SPREF("getConfString: expected string as second argument (when given)")); } QString qkey = key.toString(); if (config.contains(qkey)) { return QJSValue(config.value(qkey)); } return dval.isNull() ? QJSValue::UndefinedValue : dval; } QJSValue Scriptface::getConfBool(const QJSValue &key, const QJSValue &dval) { if (!key.isString()) { return throwError(scriptEngine, SPREF("getConfBool: expected string as first argument")); } if (!(dval.isBool() || dval.isNull())) { return throwError(scriptEngine, SPREF("getConfBool: expected boolean as second argument (when given)")); } static QStringList falsities; if (falsities.isEmpty()) { falsities.append(QString(QLatin1Char('0'))); falsities.append(QStringLiteral("no")); falsities.append(QStringLiteral("false")); } QString qkey = key.toString(); if (config.contains(qkey)) { QString qval = config.value(qkey).toLower(); return QJSValue(!falsities.contains(qval)); } return dval.isNull() ? QJSValue::UndefinedValue : dval; } QJSValue Scriptface::getConfNumber(const QJSValue &key, const QJSValue &dval) { if (!key.isString()) { return throwError(scriptEngine, SPREF("getConfNumber: expected string " "as first argument")); } if (!(dval.isNumber() || dval.isNull())) { return throwError(scriptEngine, SPREF("getConfNumber: expected number " "as second argument (when given)")); } QString qkey = key.toString(); if (config.contains(qkey)) { QString qval = config.value(qkey); bool convOk; double qnum = qval.toDouble(&convOk); if (convOk) { return QJSValue(qnum); } } return dval.isNull() ? QJSValue::UndefinedValue : dval; } // ---------------------------------------------------------------------- // Scriptface helpers to interface functions. QJSValue Scriptface::load(const QJSValueList &fnames) { if (globalKTI()->currentModulePath.isEmpty()) { return throwError(scriptEngine, SPREF("load: no current module path, aiiie...")); } for (int i = 0; i < fnames.size(); ++i) { if (!fnames[i].isString()) { return throwError(scriptEngine, SPREF("load: expected string as file name")); } } for (int i = 0; i < fnames.size(); ++i) { QString qfname = fnames[i].toString(); QString qfpath = globalKTI()->currentModulePath + QLatin1Char('/') + qfname + QLatin1String(".js"); QFile file(qfpath); if (!file.open(QIODevice::ReadOnly)) { return throwError(scriptEngine, SPREF("load: cannot read file '%1'") \ .arg(qfpath)); } QTextStream stream(&file); stream.setCodec("UTF-8"); QString source = stream.readAll(); file.close(); QJSValue comp = scriptEngine->evaluate(source, qfpath, 0); if (comp.isError()) { QString msg = comp.toString(); QString line; if (comp.isObject()) { QJSValue lval = comp.property(QStringLiteral("line")); if (lval.isNumber()) { line = QString::number(lval.toInt()); } } return throwError(scriptEngine, QStringLiteral("at %1:%2: %3") .arg(qfpath, line, msg)); } dbgout("Loaded module: %1", qfpath); } return QJSValue::UndefinedValue; } QString Scriptface::loadProps_text(const QString &fpath) { QFile file(fpath); if (!file.open(QIODevice::ReadOnly)) { return SPREF("loadProps_text: cannot read file '%1'") .arg(fpath); } QTextStream stream(&file); stream.setCodec("UTF-8"); QString s = stream.readAll(); file.close(); // Parse the map. // Should care about performance: possibly executed on each KDE // app startup and reading houndreds of thousands of characters. enum {s_nextEntry, s_nextKey, s_nextValue}; QList ekeys; // holds keys for current entry QHash props; // holds properties for current entry int slen = s.length(); int state = s_nextEntry; QByteArray pkey; QChar prop_sep, key_sep; int i = 0; while (1) { int i_checkpoint = i; if (state == s_nextEntry) { while (s[i].isSpace()) { ++i; if (i >= slen) { goto END_PROP_PARSE; } } if (i + 1 >= slen) { return SPREF("loadProps_text: unexpected end " "of file in %1").arg(fpath); } if (s[i] != QLatin1Char('#')) { // Separator characters for this entry. key_sep = s[i]; prop_sep = s[i + 1]; if (key_sep.isLetter() || prop_sep.isLetter()) { return SPREF("loadProps_text: separator " "characters must not be letters at %1:%2") .arg(fpath).arg(countLines(s, i)); } // Reset all data for current entry. ekeys.clear(); props.clear(); pkey.clear(); i += 2; state = s_nextKey; } else { // This is a comment, skip to EOL, don't change state. while (s[i] != QLatin1Char('\n')) { ++i; if (i >= slen) { goto END_PROP_PARSE; } } } } else if (state == s_nextKey) { int ip = i; // Proceed up to next key or property separator. while (s[i] != key_sep && s[i] != prop_sep) { ++i; if (i >= slen) { goto END_PROP_PARSE; } } if (s[i] == key_sep) { // This is a property key, // record for when the value gets parsed. pkey = normKeystr(s.mid(ip, i - ip), false); i += 1; state = s_nextValue; } else { // if (s[i] == prop_sep) { // This is an entry key, or end of entry. QByteArray ekey = normKeystr(s.mid(ip, i - ip), false); if (!ekey.isEmpty()) { // An entry key. ekeys.append(ekey); i += 1; state = s_nextKey; } else { // End of entry. if (ekeys.size() < 1) { return SPREF("loadProps_text: no entry key " "for entry ending at %1:%2") .arg(fpath).arg(countLines(s, i)); } // Add collected entry into global store, // once for each entry key (QHash implicitly shared). for (const QByteArray &ekey : qAsConst(ekeys)) { phraseProps[ekey] = props; } i += 1; state = s_nextEntry; } } } else if (state == s_nextValue) { int ip = i; // Proceed up to next property separator. while (s[i] != prop_sep) { ++i; if (i >= slen) { goto END_PROP_PARSE; } if (s[i] == key_sep) { return SPREF("loadProps_text: property separator " "inside property value at %1:%2") .arg(fpath).arg(countLines(s, i)); } } // Extract the property value and store the property. QByteArray pval = trimSmart(s.mid(ip, i - ip)).toUtf8(); props[pkey] = pval; i += 1; state = s_nextKey; } else { return SPREF("loadProps: internal error 10 at %1:%2") .arg(fpath).arg(countLines(s, i)); } // To avoid infinite looping and stepping out. if (i == i_checkpoint || i >= slen) { return SPREF("loadProps: internal error 20 at %1:%2") .arg(fpath).arg(countLines(s, i)); } } END_PROP_PARSE: if (state != s_nextEntry) { return SPREF("loadProps: unexpected end of file in %1") .arg(fpath); } return QString(); } // Read big-endian integer of nbytes length at position pos // in character array fc of length len. // Update position to point after the number. // In case of error, pos is set to -1. template static int bin_read_int_nbytes(const char *fc, qlonglong len, qlonglong &pos, int nbytes) { if (pos + nbytes > len) { pos = -1; return 0; } T num = qFromBigEndian((uchar *) fc + pos); pos += nbytes; return num; } // Read 64-bit big-endian integer. static quint64 bin_read_int64(const char *fc, qlonglong len, qlonglong &pos) { return bin_read_int_nbytes(fc, len, pos, 8); } // Read 32-bit big-endian integer. static quint32 bin_read_int(const char *fc, qlonglong len, qlonglong &pos) { return bin_read_int_nbytes(fc, len, pos, 4); } // Read string at position pos of character array fc of length n. // String is represented as 32-bit big-endian byte length followed by bytes. // Update position to point after the string. // In case of error, pos is set to -1. static QByteArray bin_read_string(const char *fc, qlonglong len, qlonglong &pos) { // Binary format stores strings as length followed by byte sequence. // No null-termination. int nbytes = bin_read_int(fc, len, pos); if (pos < 0) { return QByteArray(); } if (nbytes < 0 || pos + nbytes > len) { pos = -1; return QByteArray(); } QByteArray s(fc + pos, nbytes); pos += nbytes; return s; } QString Scriptface::loadProps_bin(const QString &fpath) { QFile file(fpath); if (!file.open(QIODevice::ReadOnly)) { return SPREF("loadProps: cannot read file '%1'") .arg(fpath); } // Collect header. QByteArray head(8, '0'); file.read(head.data(), head.size()); file.close(); // Choose pmap loader based on header. if (head == "TSPMAP00") { return loadProps_bin_00(fpath); } else if (head == "TSPMAP01") { return loadProps_bin_01(fpath); } else { return SPREF("loadProps: unknown version of compiled map '%1'") .arg(fpath); } } QString Scriptface::loadProps_bin_00(const QString &fpath) { QFile file(fpath); if (!file.open(QIODevice::ReadOnly)) { return SPREF("loadProps: cannot read file '%1'") .arg(fpath); } QByteArray fctmp = file.readAll(); file.close(); const char *fc = fctmp.data(); const int fclen = fctmp.size(); // Indicates stream state. qlonglong pos = 0; // Match header. QByteArray head(fc, 8); pos += 8; if (head != "TSPMAP00") { goto END_PROP_PARSE; } // Read total number of entries. int nentries; nentries = bin_read_int(fc, fclen, pos); if (pos < 0) { goto END_PROP_PARSE; } // Read all entries. for (int i = 0; i < nentries; ++i) { // Read number of entry keys and all entry keys. QList ekeys; int nekeys = bin_read_int(fc, fclen, pos); if (pos < 0) { goto END_PROP_PARSE; } ekeys.reserve(nekeys); //nekeys are appended if data is not corrupted for (int j = 0; j < nekeys; ++j) { QByteArray ekey = bin_read_string(fc, fclen, pos); if (pos < 0) { goto END_PROP_PARSE; } ekeys.append(ekey); } //dbgout("--------> ekey[0]={%1}", QString::fromUtf8(ekeys[0])); // Read number of properties and all properties. QHash props; int nprops = bin_read_int(fc, fclen, pos); if (pos < 0) { goto END_PROP_PARSE; } for (int j = 0; j < nprops; ++j) { QByteArray pkey = bin_read_string(fc, fclen, pos); if (pos < 0) { goto END_PROP_PARSE; } QByteArray pval = bin_read_string(fc, fclen, pos); if (pos < 0) { goto END_PROP_PARSE; } props[pkey] = pval; } // Add collected entry into global store, // once for each entry key (QHash implicitly shared). for (const QByteArray &ekey : qAsConst(ekeys)) { phraseProps[ekey] = props; } } END_PROP_PARSE: if (pos < 0) { return SPREF("loadProps: corrupt compiled map '%1'") .arg(fpath); } return QString(); } QString Scriptface::loadProps_bin_01(const QString &fpath) { QFile *file = new QFile(fpath); if (!file->open(QIODevice::ReadOnly)) { return SPREF("loadProps: cannot read file '%1'") .arg(fpath); } QByteArray fstr; qlonglong pos; // Read the header and number and length of entry keys. fstr = file->read(8 + 4 + 8); pos = 0; QByteArray head = fstr.left(8); pos += 8; if (head != "TSPMAP01") { return SPREF("loadProps: corrupt compiled map '%1'") .arg(fpath); } quint32 numekeys = bin_read_int(fstr, fstr.size(), pos); quint64 lenekeys = bin_read_int64(fstr, fstr.size(), pos); // Read entry keys. fstr = file->read(lenekeys); pos = 0; for (quint32 i = 0; i < numekeys; ++i) { QByteArray ekey = bin_read_string(fstr, lenekeys, pos); quint64 offset = bin_read_int64(fstr, lenekeys, pos); phraseUnparsedProps[ekey] = QPair(file, offset); } // // Read property keys. // ...when it becomes necessary loadedPmapHandles.insert(file); return QString(); } QHash Scriptface::resolveUnparsedProps(const QByteArray &phrase) { QPair ref = phraseUnparsedProps.value(phrase); QFile *file = ref.first; quint64 offset = ref.second; QHash props; if (file && file->seek(offset)) { QByteArray fstr = file->read(4 + 4); qlonglong pos = 0; quint32 numpkeys = bin_read_int(fstr, fstr.size(), pos); quint32 lenpkeys = bin_read_int(fstr, fstr.size(), pos); fstr = file->read(lenpkeys); pos = 0; for (quint32 i = 0; i < numpkeys; ++i) { QByteArray pkey = bin_read_string(fstr, lenpkeys, pos); QByteArray pval = bin_read_string(fstr, lenpkeys, pos); props[pkey] = pval; } phraseProps[phrase] = props; phraseUnparsedProps.remove(phrase); } return props; } #include "ktranscript.moc" diff --git a/src/kuitmarkup.cpp b/src/kuitmarkup.cpp index ce5b313..8b4a302 100644 --- a/src/kuitmarkup.cpp +++ b/src/kuitmarkup.cpp @@ -1,1676 +1,1667 @@ /* This file is part of the KDE libraries Copyright (C) 2007, 2013 Chusslove Illich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ki18n_logging_kuit.h" #define QL1S(x) QLatin1String(x) #define QSL(x) QStringLiteral(x) #define QL1C(x) QLatin1Char(x) QString Kuit::escape(const QString &text) { int tlen = text.length(); QString ntext; ntext.reserve(tlen); for (int i = 0; i < tlen; ++i) { QChar c = text[i]; if (c == QL1C('&')) { ntext += QStringLiteral("&"); } else if (c == QL1C('<')) { ntext += QStringLiteral("<"); } else if (c == QL1C('>')) { ntext += QStringLiteral(">"); } else if (c == QL1C('\'')) { ntext += QStringLiteral("'"); } else if (c == QL1C('"')) { ntext += QStringLiteral("""); } else { ntext += c; } } return ntext; } // Truncates the string, for output of long messages. // (But don't truncate too much otherwise it's impossible to determine // which message is faulty if many messages have the same beginning). static QString shorten(const QString &str) { const int maxlen = 80; if (str.length() <= maxlen) { return str; } else { return str.leftRef(maxlen) + QSL("..."); } } static void parseUiMarker(const QString &context_, QString &roleName, QString &cueName, QString &formatName) { // UI marker is in the form @role:cue/format, // and must start just after any leading whitespace in the context string. // Names remain untouched if UI marker is not found. // Normalize all names, trimmed, all lower-case QString context = context_.trimmed().toLower(); static const QRegularExpression rolesRx(QStringLiteral("^@(\\w+):?(\\w*)/?(\\w*)")); const QRegularExpressionMatch match = rolesRx.match(context); if (match.hasMatch()) { roleName = match.captured(1); cueName = match.captured(2); formatName = match.captured(3); } } // Custom entity resolver for QXmlStreamReader. class KuitEntityResolver : public QXmlStreamEntityResolver { public: void setEntities(const QHash &entities) { entityMap = entities; } QString resolveUndeclaredEntity(const QString &name) override { QString value = entityMap.value(name); // This will return empty string if the entity name is not known, // which will make QXmlStreamReader signal unknown entity error. return value; } private: QHash entityMap; }; namespace Kuit { enum Role { // UI marker roles UndefinedRole, ActionRole, TitleRole, OptionRole, LabelRole, ItemRole, InfoRole }; enum Cue { // UI marker subcues UndefinedCue, ButtonCue, InmenuCue, IntoolbarCue, WindowCue, MenuCue, TabCue, GroupCue, ColumnCue, RowCue, SliderCue, SpinboxCue, ListboxCue, TextboxCue, ChooserCue, CheckCue, RadioCue, InlistboxCue, IntableCue, InrangeCue, IntextCue, ValuesuffixCue, TooltipCue, WhatsthisCue, PlaceholderCue, StatusCue, ProgressCue, TipofthedayCue, CreditCue, ShellCue }; } class KuitStaticData { public: QHash xmlEntities; QHash xmlEntitiesInverse; KuitEntityResolver xmlEntityResolver; QHash rolesByName; QHash cuesByName; QHash formatsByName; QHash namesByFormat; QHash > knownRoleCues; QHash comboKeyDelim; QHash guiPathDelim; QHash keyNames; QHash domainSetups; KuitStaticData(); ~KuitStaticData(); KuitStaticData(const KuitStaticData &) = delete; KuitStaticData &operator=(const KuitStaticData &) = delete; void setXmlEntityData(); void setUiMarkerData(); void setTextTransformData(); QString toKeyCombo(const QStringList &languages, const QString &shstr, Kuit::VisualFormat format); QString toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format); }; KuitStaticData::KuitStaticData() { setXmlEntityData(); setUiMarkerData(); setTextTransformData(); } KuitStaticData::~KuitStaticData() { qDeleteAll(domainSetups); } void KuitStaticData::setXmlEntityData() { QString LT = QStringLiteral("lt"); QString GT = QStringLiteral("gt"); QString AMP = QStringLiteral("amp"); QString APOS = QStringLiteral("apos"); QString QUOT = QStringLiteral("quot"); // Default XML entities, direct and inverse mapping. xmlEntities[LT] = QString(QL1C('<')); xmlEntities[GT] = QString(QL1C('>')); xmlEntities[AMP] = QString(QL1C('&')); xmlEntities[APOS] = QString(QL1C('\'')); xmlEntities[QUOT] = QString(QL1C('"')); xmlEntitiesInverse[QString(QL1C('<'))] = LT; xmlEntitiesInverse[QString(QL1C('>'))] = GT; xmlEntitiesInverse[QString(QL1C('&'))] = AMP; xmlEntitiesInverse[QString(QL1C('\''))] = APOS; xmlEntitiesInverse[QString(QL1C('"'))] = QUOT; // Custom XML entities. xmlEntities[QStringLiteral("nbsp")] = QString(QChar(0xa0)); xmlEntityResolver.setEntities(xmlEntities); } void KuitStaticData::setUiMarkerData() { using namespace Kuit; // Role names and their available subcues. #undef SET_ROLE #define SET_ROLE(role, name, cues) do { \ rolesByName[name] = role; \ knownRoleCues[role] << cues; \ } while (0) SET_ROLE(ActionRole, QStringLiteral("action"), ButtonCue << InmenuCue << IntoolbarCue); SET_ROLE(TitleRole, QStringLiteral("title"), WindowCue << MenuCue << TabCue << GroupCue << ColumnCue << RowCue); SET_ROLE(LabelRole, QStringLiteral("label"), SliderCue << SpinboxCue << ListboxCue << TextboxCue << ChooserCue); SET_ROLE(OptionRole, QStringLiteral("option"), CheckCue << RadioCue); SET_ROLE(ItemRole, QStringLiteral("item"), InmenuCue << InlistboxCue << IntableCue << InrangeCue << IntextCue << ValuesuffixCue); SET_ROLE(InfoRole, QStringLiteral("info"), TooltipCue << WhatsthisCue << PlaceholderCue << StatusCue << ProgressCue << TipofthedayCue << CreditCue << ShellCue); // Cue names. #undef SET_CUE #define SET_CUE(cue, name) do { \ cuesByName[name] = cue; \ } while (0) SET_CUE(ButtonCue, QStringLiteral("button")); SET_CUE(InmenuCue, QStringLiteral("inmenu")); SET_CUE(IntoolbarCue, QStringLiteral("intoolbar")); SET_CUE(WindowCue, QStringLiteral("window")); SET_CUE(MenuCue, QStringLiteral("menu")); SET_CUE(TabCue, QStringLiteral("tab")); SET_CUE(GroupCue, QStringLiteral("group")); SET_CUE(ColumnCue, QStringLiteral("column")); SET_CUE(RowCue, QStringLiteral("row")); SET_CUE(SliderCue, QStringLiteral("slider")); SET_CUE(SpinboxCue, QStringLiteral("spinbox")); SET_CUE(ListboxCue, QStringLiteral("listbox")); SET_CUE(TextboxCue, QStringLiteral("textbox")); SET_CUE(ChooserCue, QStringLiteral("chooser")); SET_CUE(CheckCue, QStringLiteral("check")); SET_CUE(RadioCue, QStringLiteral("radio")); SET_CUE(InlistboxCue, QStringLiteral("inlistbox")); SET_CUE(IntableCue, QStringLiteral("intable")); SET_CUE(InrangeCue, QStringLiteral("inrange")); SET_CUE(IntextCue, QStringLiteral("intext")); SET_CUE(ValuesuffixCue, QStringLiteral("valuesuffix")); SET_CUE(TooltipCue, QStringLiteral("tooltip")); SET_CUE(WhatsthisCue, QStringLiteral("whatsthis")); SET_CUE(PlaceholderCue, QStringLiteral("placeholder")); SET_CUE(StatusCue, QStringLiteral("status")); SET_CUE(ProgressCue, QStringLiteral("progress")); SET_CUE(TipofthedayCue, QStringLiteral("tipoftheday")); SET_CUE(CreditCue, QStringLiteral("credit")); SET_CUE(ShellCue, QStringLiteral("shell")); // Format names. #undef SET_FORMAT #define SET_FORMAT(format, name) do { \ formatsByName[name] = format; \ namesByFormat[format] = name; \ } while (0) SET_FORMAT(UndefinedFormat, QStringLiteral("undefined")); SET_FORMAT(PlainText, QStringLiteral("plain")); SET_FORMAT(RichText, QStringLiteral("rich")); SET_FORMAT(TermText, QStringLiteral("term")); } void KuitStaticData::setTextTransformData() { // i18n: Decide which string is used to delimit keys in a keyboard // shortcut (e.g. + in Ctrl+Alt+Tab) in plain text. comboKeyDelim[Kuit::PlainText] = ki18nc("shortcut-key-delimiter/plain", "+"); comboKeyDelim[Kuit::TermText] = comboKeyDelim[Kuit::PlainText]; // i18n: Decide which string is used to delimit keys in a keyboard // shortcut (e.g. + in Ctrl+Alt+Tab) in rich text. comboKeyDelim[Kuit::RichText] = ki18nc("shortcut-key-delimiter/rich", "+"); // i18n: Decide which string is used to delimit elements in a GUI path // (e.g. -> in "Go to Settings->Advanced->Core tab.") in plain text. guiPathDelim[Kuit::PlainText] = ki18nc("gui-path-delimiter/plain", "→"); guiPathDelim[Kuit::TermText] = guiPathDelim[Kuit::PlainText]; // i18n: Decide which string is used to delimit elements in a GUI path // (e.g. -> in "Go to Settings->Advanced->Core tab.") in rich text. guiPathDelim[Kuit::RichText] = ki18nc("gui-path-delimiter/rich", "→"); // NOTE: The '→' glyph seems to be available in all widespread fonts. // Collect keyboard key names. #undef SET_KEYNAME #define SET_KEYNAME(rawname) do { \ /* Normalize key, trim and all lower-case. */ \ QString normname = QStringLiteral(rawname).trimmed().toLower(); \ keyNames[normname] = ki18nc("keyboard-key-name", rawname); \ } while (0) // Now we need I18NC_NOOP that does remove context. #undef I18NC_NOOP #define I18NC_NOOP(ctxt, msg) msg SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Alt")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "AltGr")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Backspace")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "CapsLock")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Control")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Ctrl")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Del")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Delete")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Down")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "End")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Enter")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Esc")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Escape")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Home")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Hyper")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Ins")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Insert")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Left")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Menu")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Meta")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "NumLock")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PageDown")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PageUp")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PgDown")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PgUp")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PauseBreak")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PrintScreen")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "PrtScr")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Return")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Right")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "ScrollLock")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Shift")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Space")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Super")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "SysReq")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Tab")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Up")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "Win")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F1")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F2")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F3")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F4")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F5")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F6")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F7")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F8")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F9")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F10")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F11")); SET_KEYNAME(I18NC_NOOP("keyboard-key-name", "F12")); // TODO: Add rest of the key names? } QString KuitStaticData::toKeyCombo(const QStringList &languages, const QString &shstr, Kuit::VisualFormat format) { // Take '+' or '-' as input shortcut delimiter, // whichever is first encountered. static const QRegularExpression delimRx(QStringLiteral("[+-]")); const QRegularExpressionMatch match = delimRx.match(shstr); QStringList keys; if (match.hasMatch()) { // delimiter found, multi-key shortcut const QString oldDelim = match.captured(0); keys = shstr.split(oldDelim, QString::SkipEmptyParts); } else { // single-key shortcut, no delimiter found keys.append(shstr); } for (int i = 0; i < keys.size(); ++i) { // Normalize key, trim and all lower-case. const QString nkey = keys.at(i).trimmed().toLower(); keys[i] = keyNames.contains(nkey) ? keyNames[nkey].toString(languages) : keys.at(i).trimmed(); } const QString delim = comboKeyDelim.value(format).toString(languages); return keys.join(delim); } QString KuitStaticData::toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format) { // Take '/', '|' or "->" as input path delimiter, // whichever is first encountered. static const QRegularExpression delimRx(QStringLiteral("\\||->")); const QRegularExpressionMatch match = delimRx.match(inpstr); if (match.hasMatch()) { // multi-element path const QString oldDelim = match.captured(0); QStringList guiels = inpstr.split(oldDelim, QString::SkipEmptyParts); const QString delim = guiPathDelim.value(format).toString(languages); return guiels.join(delim); } // single-element path, no delimiter found return inpstr; } Q_GLOBAL_STATIC(KuitStaticData, staticData) static QString attributeSetKey(const QStringList &attribNames_) { QStringList attribNames = attribNames_; std::sort(attribNames.begin(), attribNames.end()); QString key = QL1C('[') + attribNames.join(QL1C(' ')) + QL1C(']'); return key; } class KuitTag { public: QString name; Kuit::TagClass type; QSet knownAttribs; QHash > attributeOrders; QHash > patterns; QHash > formatters; int leadingNewlines; QString format(const QStringList &languages, const QHash &attributes, const QString &text, const QStringList &tagPath, Kuit::VisualFormat format) const; }; QString KuitTag::format(const QStringList &languages, const QHash &attributes, const QString &text, const QStringList &tagPath, Kuit::VisualFormat format) const { KuitStaticData *s = staticData(); QString formattedText = text; QString attribKey = attributeSetKey(attributes.keys()); const QHash pattern = patterns.value(attribKey); if (pattern.contains(format)) { QString modText; Kuit::TagFormatter formatter = formatters.value(attribKey).value(format); if (formatter != nullptr) { modText = formatter(languages, name, attributes, text, tagPath, format); } else { modText = text; } KLocalizedString aggText = pattern.value(format); // line below is first-aid fix.for e.g. . // TODO: proper handling of boolean attributes still needed aggText = aggText.relaxSubs(); if (!aggText.isEmpty()) { aggText = aggText.subs(modText); const QStringList attributeOrder = attributeOrders.value(attribKey).value(format); for (const QString &attribName : attributeOrder) { aggText = aggText.subs(attributes.value(attribName)); } formattedText = aggText.ignoreMarkup().toString(languages); } else { formattedText = modText; } } else if (patterns.contains(attribKey)) { qCWarning(KI18N_KUIT) << QStringLiteral( "Undefined visual format for tag <%1> and attribute combination %2: %3.") .arg(name, attribKey, s->namesByFormat.value(format)); } else { qCWarning(KI18N_KUIT) << QStringLiteral( "Undefined attribute combination for tag <%1>: %2.") .arg(name, attribKey); } return formattedText; } KuitSetup &Kuit::setupForDomain(const QByteArray& domain) { KuitStaticData *s = staticData(); KuitSetup *setup = s->domainSetups.value(domain); if (!setup) { setup = new KuitSetup(domain); s->domainSetups.insert(domain, setup); } return *setup; } KuitSetup &Kuit::setupForDomain(const char *domain) { return setupForDomain(QByteArray(domain)); } class KuitSetupPrivate { public: void setTagPattern(const QString &tagName, const QStringList &attribNames, Kuit::VisualFormat format, const KLocalizedString &pattern, Kuit::TagFormatter formatter, int leadingNewlines); void setTagClass(const QString &tagName, Kuit::TagClass aClass); void setFormatForMarker(const QString &marker, Kuit::VisualFormat format); void setDefaultMarkup(); void setDefaultFormats(); QByteArray domain; QHash knownTags; QHash > formatsByRoleCue; }; void KuitSetupPrivate::setTagPattern(const QString &tagName, const QStringList &attribNames_, Kuit::VisualFormat format, const KLocalizedString &pattern, Kuit::TagFormatter formatter, int leadingNewlines_) { bool isNewTag = knownTags.contains(tagName); KuitTag &tag = knownTags[tagName]; if (isNewTag) { tag.name = tagName; tag.type = Kuit::PhraseTag; } QStringList attribNames = attribNames_; attribNames.removeAll(QString()); for (const QString &attribName : qAsConst(attribNames)) { tag.knownAttribs.insert(attribName); } QString attribKey = attributeSetKey(attribNames); tag.attributeOrders[attribKey][format] = attribNames; tag.patterns[attribKey][format] = pattern; tag.formatters[attribKey][format] = formatter; tag.leadingNewlines = leadingNewlines_; } void KuitSetupPrivate::setTagClass(const QString &tagName, Kuit::TagClass aClass) { bool isNewTag = knownTags.contains(tagName); KuitTag &tag = knownTags[tagName]; if (isNewTag) { tag.name = tagName; } tag.type = aClass; } void KuitSetupPrivate::setFormatForMarker(const QString &marker, Kuit::VisualFormat format) { KuitStaticData *s = staticData(); QString roleName, cueName, formatName; parseUiMarker(marker, roleName, cueName, formatName); Kuit::Role role; if (s->rolesByName.contains(roleName)) { role = s->rolesByName.value(roleName); } else if (!roleName.isEmpty()) { qCWarning(KI18N_KUIT) << QStringLiteral( "Unknown role '@%1' in UI marker {%2}, visual format not set.") .arg(roleName, marker); return; } else { qCWarning(KI18N_KUIT) << QStringLiteral( "Empty role in UI marker {%1}, visual format not set.") .arg(marker); return; } Kuit::Cue cue; if (s->cuesByName.contains(cueName)) { cue = s->cuesByName.value(cueName); if (!s->knownRoleCues.value(role).contains(cue)) { qCWarning(KI18N_KUIT) << QStringLiteral( "Subcue ':%1' does not belong to role '@%2' in UI marker {%3}, visual format not set.") .arg(cueName, roleName, marker); return; } } else if (!cueName.isEmpty()) { qCWarning(KI18N_KUIT) << QStringLiteral( "Unknown subcue ':%1' in UI marker {%2}, visual format not set.") .arg(cueName, marker); return; } else { cue = Kuit::UndefinedCue; } formatsByRoleCue[role][cue] = format; } #define TAG_FORMATTER_ARGS \ const QStringList &languages, \ const QString &tagName, \ const QHash &attributes, \ const QString &text, \ const QStringList &tagPath, \ Kuit::VisualFormat format static QString tagFormatterFilename(TAG_FORMATTER_ARGS) { Q_UNUSED(languages); Q_UNUSED(tagName); Q_UNUSED(attributes); Q_UNUSED(tagPath); #ifdef Q_OS_WIN // with rich text the path can include ... which will be replaced by ...<\foo> on Windows! // the same problem also happens for tags such as
-> if (format == Kuit::RichText) { // replace all occurrences of "" to make sure toNativeSeparators() doesn't destroy XML markup const auto KUIT_CLOSE_XML_REPLACEMENT = QStringLiteral("__kuit_close_xml_tag__"); const auto KUIT_NOTEXT_XML_REPLACEMENT = QStringLiteral("__kuit_notext_xml_tag__"); QString result = text; result.replace(QStringLiteral(""), KUIT_NOTEXT_XML_REPLACEMENT); result = QDir::toNativeSeparators(result); result.replace(KUIT_CLOSE_XML_REPLACEMENT, QStringLiteral("")); return result; } #else Q_UNUSED(format); #endif return QDir::toNativeSeparators(text); } static QString tagFormatterShortcut(TAG_FORMATTER_ARGS) { Q_UNUSED(tagName); Q_UNUSED(attributes); Q_UNUSED(tagPath); KuitStaticData *s = staticData(); return s->toKeyCombo(languages, text, format); } static QString tagFormatterInterface(TAG_FORMATTER_ARGS) { Q_UNUSED(tagName); Q_UNUSED(attributes); Q_UNUSED(tagPath); KuitStaticData *s = staticData(); return s->toInterfacePath(languages, text, format); } void KuitSetupPrivate::setDefaultMarkup() { using namespace Kuit; const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__"); const QString TITLE = QStringLiteral("title"); const QString EMPHASIS = QStringLiteral("emphasis"); const QString COMMAND = QStringLiteral("command"); const QString WARNING = QStringLiteral("warning"); const QString LINK = QStringLiteral("link"); const QString NOTE = QStringLiteral("note"); // Macro to hide message from extraction. #define HI18NC ki18nc // Macro to expedite setting the patterns. #undef SET_PATTERN #define SET_PATTERN(tagName, attribNames_, format, pattern, formatter, leadNl) \ do { \ QStringList attribNames; \ attribNames << attribNames_; \ setTagPattern(tagName, attribNames, format, pattern, formatter, leadNl); \ /* Make TermText pattern same as PlainText if not explicitly given. */ \ KuitTag &tag = knownTags[tagName]; \ QString attribKey = attributeSetKey(attribNames); \ if (format == PlainText && !tag.patterns[attribKey].contains(TermText)) { \ setTagPattern(tagName, attribNames, TermText, pattern, formatter, leadNl); \ } \ } while (0) // NOTE: The following "i18n:" comments are oddly placed in order that // xgettext extracts them properly. // -------> Internal top tag setTagClass(INTERNAL_TOP_TAG_NAME, StructTag); setTagClass(INTERNAL_TOP_TAG_NAME, StructTag); SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), PlainText, HI18NC("tag-format-pattern <> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 0); SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), RichText, HI18NC("tag-format-pattern <> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 0); // -------> Title setTagClass(TITLE, StructTag); SET_PATTERN(TITLE, QString(), PlainText, ki18nc("tag-format-pattern plain", // i18n: The messages with context "tag-format-pattern <tag ...> format" // are KUIT patterns for formatting the text found inside KUIT tags. // The format is either "plain" or "rich", and tells if the pattern // is used for plain text or rich text (which can use HTML tags). // You may be in general satisfied with the patterns as they are in the // original. Some things you may consider changing: // - the proper quotes, those used in msgid are English-standard // - the <i> and <b> tags, does your language script work well with them? "== %1 =="), nullptr, 2); SET_PATTERN(TITLE, QString(), RichText, ki18nc("tag-format-pattern <title> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<h2>%1</h2>"), nullptr, 2); // -------> Subtitle setTagClass(QSL("subtitle"), StructTag); SET_PATTERN(QSL("subtitle"), QString(), PlainText, ki18nc("tag-format-pattern <subtitle> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "~ %1 ~"), nullptr, 2); SET_PATTERN(QSL("subtitle"), QString(), RichText, ki18nc("tag-format-pattern <subtitle> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<h3>%1</h3>"), nullptr, 2); // -------> Para setTagClass(QSL("para"), StructTag); SET_PATTERN(QSL("para"), QString(), PlainText, ki18nc("tag-format-pattern <para> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 2); SET_PATTERN(QSL("para"), QString(), RichText, ki18nc("tag-format-pattern <para> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<p>%1</p>"), nullptr, 2); // -------> List setTagClass(QSL("list"), StructTag); SET_PATTERN(QSL("list"), QString(), PlainText, ki18nc("tag-format-pattern <list> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 1); SET_PATTERN(QSL("list"), QString(), RichText, ki18nc("tag-format-pattern <list> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<ul>%1</ul>"), nullptr, 1); // -------> Item setTagClass(QSL("item"), StructTag); SET_PATTERN(QSL("item"), QString(), PlainText, ki18nc("tag-format-pattern <item> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. " * %1"), nullptr, 1); SET_PATTERN(QSL("item"), QString(), RichText, ki18nc("tag-format-pattern <item> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<li>%1</li>"), nullptr, 1); // -------> Note SET_PATTERN(NOTE, QString(), PlainText, ki18nc("tag-format-pattern <note> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "Note: %1"), nullptr, 0); SET_PATTERN(NOTE, QString(), RichText, ki18nc("tag-format-pattern <note> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<i>Note</i>: %1"), nullptr, 0); SET_PATTERN(NOTE, QSL("label"), PlainText, ki18nc("tag-format-pattern <note label=> plain\n" "%1 is the text, %2 is the note label", // i18n: KUIT pattern, see the comment to the first of these entries above. "%2: %1"), nullptr, 0); SET_PATTERN(NOTE, QSL("label"), RichText, ki18nc("tag-format-pattern <note label=> rich\n" "%1 is the text, %2 is the note label", // i18n: KUIT pattern, see the comment to the first of these entries above. "<i>%2</i>: %1"), nullptr, 0); // -------> Warning SET_PATTERN(WARNING, QString(), PlainText, ki18nc("tag-format-pattern <warning> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "WARNING: %1"), nullptr, 0); SET_PATTERN(WARNING, QString(), RichText, ki18nc("tag-format-pattern <warning> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<b>Warning</b>: %1"), nullptr, 0); SET_PATTERN(WARNING, QSL("label"), PlainText, ki18nc("tag-format-pattern <warning label=> plain\n" "%1 is the text, %2 is the warning label", // i18n: KUIT pattern, see the comment to the first of these entries above. "%2: %1"), nullptr, 0); SET_PATTERN(WARNING, QSL("label"), RichText, ki18nc("tag-format-pattern <warning label=> rich\n" "%1 is the text, %2 is the warning label", // i18n: KUIT pattern, see the comment to the first of these entries above. "<b>%2</b>: %1"), nullptr, 0); // -------> Link SET_PATTERN(LINK, QString(), PlainText, ki18nc("tag-format-pattern <link> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 0); SET_PATTERN(LINK, QString(), RichText, ki18nc("tag-format-pattern <link> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<a href=\"%1\">%1</a>"), nullptr, 0); SET_PATTERN(LINK, QSL("url"), PlainText, ki18nc("tag-format-pattern <link url=> plain\n" "%1 is the descriptive text, %2 is the URL", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1 (%2)"), nullptr, 0); SET_PATTERN(LINK, QSL("url"), RichText, ki18nc("tag-format-pattern <link url=> rich\n" "%1 is the descriptive text, %2 is the URL", // i18n: KUIT pattern, see the comment to the first of these entries above. "<a href=\"%2\">%1</a>"), nullptr, 0); // -------> Filename SET_PATTERN(QSL("filename"), QString(), PlainText, ki18nc("tag-format-pattern <filename> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "‘%1’"), tagFormatterFilename, 0); SET_PATTERN(QSL("filename"), QString(), RichText, ki18nc("tag-format-pattern <filename> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<tt>%1</tt>"), tagFormatterFilename, 0); // -------> Application SET_PATTERN(QSL("application"), QString(), PlainText, ki18nc("tag-format-pattern <application> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 0); SET_PATTERN(QSL("application"), QString(), RichText, ki18nc("tag-format-pattern <application> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 0); // -------> Command SET_PATTERN(COMMAND, QString(), PlainText, ki18nc("tag-format-pattern <command> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), nullptr, 0); SET_PATTERN(COMMAND, QString(), RichText, ki18nc("tag-format-pattern <command> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<tt>%1</tt>"), nullptr, 0); SET_PATTERN(COMMAND, QSL("section"), PlainText, ki18nc("tag-format-pattern <command section=> plain\n" "%1 is the command name, %2 is its man section", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1(%2)"), nullptr, 0); SET_PATTERN(COMMAND, QSL("section"), RichText, ki18nc("tag-format-pattern <command section=> rich\n" "%1 is the command name, %2 is its man section", // i18n: KUIT pattern, see the comment to the first of these entries above. "<tt>%1(%2)</tt>"), nullptr, 0); // -------> Resource SET_PATTERN(QSL("resource"), QString(), PlainText, ki18nc("tag-format-pattern <resource> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "“%1”"), nullptr, 0); SET_PATTERN(QSL("resource"), QString(), RichText, ki18nc("tag-format-pattern <resource> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "“%1”"), nullptr, 0); // -------> Icode SET_PATTERN(QSL("icode"), QString(), PlainText, ki18nc("tag-format-pattern <icode> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "“%1”"), nullptr, 0); SET_PATTERN(QSL("icode"), QString(), RichText, ki18nc("tag-format-pattern <icode> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<tt>%1</tt>"), nullptr, 0); // -------> Bcode SET_PATTERN(QSL("bcode"), QString(), PlainText, ki18nc("tag-format-pattern <bcode> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "\n%1\n"), nullptr, 2); SET_PATTERN(QSL("bcode"), QString(), RichText, ki18nc("tag-format-pattern <bcode> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<pre>%1</pre>"), nullptr, 2); // -------> Shortcut SET_PATTERN(QSL("shortcut"), QString(), PlainText, ki18nc("tag-format-pattern <shortcut> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1"), tagFormatterShortcut, 0); SET_PATTERN(QSL("shortcut"), QString(), RichText, ki18nc("tag-format-pattern <shortcut> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<b>%1</b>"), tagFormatterShortcut, 0); // -------> Interface SET_PATTERN(QSL("interface"), QString(), PlainText, ki18nc("tag-format-pattern <interface> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "|%1|"), tagFormatterInterface, 0); SET_PATTERN(QSL("interface"), QString(), RichText, ki18nc("tag-format-pattern <interface> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<i>%1</i>"), tagFormatterInterface, 0); // -------> Emphasis SET_PATTERN(EMPHASIS, QString(), PlainText, ki18nc("tag-format-pattern <emphasis> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "*%1*"), nullptr, 0); SET_PATTERN(EMPHASIS, QString(), RichText, ki18nc("tag-format-pattern <emphasis> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<i>%1</i>"), nullptr, 0); SET_PATTERN(EMPHASIS, QSL("strong"), PlainText, ki18nc("tag-format-pattern <emphasis-strong> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "**%1**"), nullptr, 0); SET_PATTERN(EMPHASIS, QSL("strong"), RichText, ki18nc("tag-format-pattern <emphasis-strong> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<b>%1</b>"), nullptr, 0); // -------> Placeholder SET_PATTERN(QSL("placeholder"), QString(), PlainText, ki18nc("tag-format-pattern <placeholder> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "<%1>"), nullptr, 0); SET_PATTERN(QSL("placeholder"), QString(), RichText, ki18nc("tag-format-pattern <placeholder> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<<i>%1</i>>"), nullptr, 0); // -------> Email SET_PATTERN(QSL("email"), QString(), PlainText, ki18nc("tag-format-pattern <email> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "<%1>"), nullptr, 0); SET_PATTERN(QSL("email"), QString(), RichText, ki18nc("tag-format-pattern <email> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<<a href=\"mailto:%1\">%1</a>>"), nullptr, 0); SET_PATTERN(QSL("email"), QSL("address"), PlainText, ki18nc("tag-format-pattern <email address=> plain\n" "%1 is name, %2 is address", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1 <%2>"), nullptr, 0); SET_PATTERN(QSL("email"), QSL("address"), RichText, ki18nc("tag-format-pattern <email address=> rich\n" "%1 is name, %2 is address", // i18n: KUIT pattern, see the comment to the first of these entries above. "<a href=\"mailto:%2\">%1</a>"), nullptr, 0); // -------> Envar SET_PATTERN(QSL("envar"), QString(), PlainText, ki18nc("tag-format-pattern <envar> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "$%1"), nullptr, 0); SET_PATTERN(QSL("envar"), QString(), RichText, ki18nc("tag-format-pattern <envar> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<tt>$%1</tt>"), nullptr, 0); // -------> Message SET_PATTERN(QSL("message"), QString(), PlainText, ki18nc("tag-format-pattern <message> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "/%1/"), nullptr, 0); SET_PATTERN(QSL("message"), QString(), RichText, ki18nc("tag-format-pattern <message> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "<i>%1</i>"), nullptr, 0); // -------> Nl SET_PATTERN(QSL("nl"), QString(), PlainText, ki18nc("tag-format-pattern <nl> plain", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1\n"), nullptr, 0); SET_PATTERN(QSL("nl"), QString(), RichText, ki18nc("tag-format-pattern <nl> rich", // i18n: KUIT pattern, see the comment to the first of these entries above. "%1<br/>"), nullptr, 0); } void KuitSetupPrivate::setDefaultFormats() { using namespace Kuit; // Setup formats by role. formatsByRoleCue[ActionRole][UndefinedCue] = PlainText; formatsByRoleCue[TitleRole][UndefinedCue] = PlainText; formatsByRoleCue[LabelRole][UndefinedCue] = PlainText; formatsByRoleCue[OptionRole][UndefinedCue] = PlainText; formatsByRoleCue[ItemRole][UndefinedCue] = PlainText; formatsByRoleCue[InfoRole][UndefinedCue] = RichText; // Setup override formats by subcue. formatsByRoleCue[InfoRole][StatusCue] = PlainText; formatsByRoleCue[InfoRole][ProgressCue] = PlainText; formatsByRoleCue[InfoRole][CreditCue] = PlainText; formatsByRoleCue[InfoRole][ShellCue] = TermText; } KuitSetup::KuitSetup(const QByteArray &domain) : d(new KuitSetupPrivate) { d->domain = domain; d->setDefaultMarkup(); d->setDefaultFormats(); } KuitSetup::~KuitSetup() { delete d; } void KuitSetup::setTagPattern(const QString &tagName, const QStringList &attribNames, Kuit::VisualFormat format, const KLocalizedString &pattern, Kuit::TagFormatter formatter, int leadingNewlines) { d->setTagPattern(tagName, attribNames, format, pattern, formatter, leadingNewlines); } void KuitSetup::setTagClass(const QString &tagName, Kuit::TagClass aClass) { d->setTagClass(tagName, aClass); } void KuitSetup::setFormatForMarker(const QString &marker, Kuit::VisualFormat format) { d->setFormatForMarker(marker, format); } class KuitFormatterPrivate { public: KuitFormatterPrivate(const QString &language); QString format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const; // Get metatranslation (formatting patterns, etc.) QString metaTr(const char *context, const char *text) const; // Set visual formatting patterns for text within tags. void setFormattingPatterns(); // Set data used in transformation of text within tags. void setTextTransformData(); // Determine visual format by parsing the UI marker in the context. static Kuit::VisualFormat formatFromUiMarker(const QString &context, const KuitSetup &setup); // Determine if text has block structure (multiple paragraphs, etc). static bool determineIsStructured(const QString &text, const KuitSetup &setup); // Format KUIT text into visual text. QString toVisualText(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const; // Final touches to the formatted text. QString finalizeVisualText(const QString &ftext, Kuit::VisualFormat format) const; // In case of markup errors, try to make result not look too bad. QString salvageMarkup(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const; // Data for XML parsing state. class OpenEl { public: enum Handling { Proper, Ignored, Dropout }; KuitTag tag; QString name; QHash<QString, QString> attributes; QString attribStr; Handling handling; QString formattedText; QStringList tagPath; }; // Gather data about current element for the parse state. KuitFormatterPrivate::OpenEl parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const; // Format text of the element. QString formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const; // Count number of newlines at start and at end of text. static void countWrappingNewlines(const QString &ptext, int &numle, int &numtr); private: QString language; QStringList languageAsList; QHash<Kuit::VisualFormat, QString> comboKeyDelim; QHash<Kuit::VisualFormat, QString> guiPathDelim; QHash<QString, QString> keyNames; }; KuitFormatterPrivate::KuitFormatterPrivate(const QString &language_) : language(language_) { } QString KuitFormatterPrivate::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const { const KuitSetup &setup = Kuit::setupForDomain(domain); // If format is undefined, determine it based on UI marker inside context. Kuit::VisualFormat resolvedFormat = format; if (resolvedFormat == Kuit::UndefinedFormat) { resolvedFormat = formatFromUiMarker(context, setup); } // Quick check: are there any tags at all? QString ftext; if (text.indexOf(QL1C('<')) < 0) { ftext = finalizeVisualText(text, resolvedFormat); } else { // Format the text. ftext = toVisualText(text, resolvedFormat, setup); if (ftext.isEmpty()) { // error while processing markup ftext = salvageMarkup(text, resolvedFormat, setup); } } return ftext; } Kuit::VisualFormat KuitFormatterPrivate::formatFromUiMarker(const QString &context, const KuitSetup &setup) { KuitStaticData *s = staticData(); QString roleName, cueName, formatName; parseUiMarker(context, roleName, cueName, formatName); // Set role from name. Kuit::Role role = s->rolesByName.value(roleName, Kuit::UndefinedRole); if (role == Kuit::UndefinedRole) { // unknown role if (!roleName.isEmpty()) { qCWarning(KI18N_KUIT) << QStringLiteral( "Unknown role '@%1' in UI marker in context {%2}.") .arg(roleName, shorten(context)); } } // Set subcue from name. Kuit::Cue cue; if (role != Kuit::UndefinedRole) { cue = s->cuesByName.value(cueName, Kuit::UndefinedCue); if (cue != Kuit::UndefinedCue) { // known subcue if (!s->knownRoleCues.value(role).contains(cue)) { cue = Kuit::UndefinedCue; qCWarning(KI18N_KUIT) << QStringLiteral( "Subcue ':%1' does not belong to role '@%2' in UI marker in context {%3}.") .arg(cueName, roleName, shorten(context)); } } else { // unknown or not given subcue if (!cueName.isEmpty()) { qCWarning(KI18N_KUIT) << QStringLiteral( "Unknown subcue ':%1' in UI marker in context {%2}.") .arg(cueName, shorten(context)); } } } else { // Bad role, silently ignore the cue. cue = Kuit::UndefinedCue; } // Set format from name, or by derivation from contex/subcue. Kuit::VisualFormat format = s->formatsByName.value(formatName, Kuit::UndefinedFormat); if (format == Kuit::UndefinedFormat) { // unknown or not given format // Check first if there is a format defined for role/subcue // combination, then for role only, otherwise default to undefined. if (setup.d->formatsByRoleCue.contains(role)) { if (setup.d->formatsByRoleCue.value(role).contains(cue)) { format = setup.d->formatsByRoleCue.value(role).value(cue); } else { format = setup.d->formatsByRoleCue.value(role).value(Kuit::UndefinedCue); } } if (!formatName.isEmpty()) { qCWarning(KI18N_KUIT) << QStringLiteral( "Unknown format '/%1' in UI marker for message {%2}.") .arg(formatName, shorten(context)); } } if (format == Kuit::UndefinedFormat) { format = Kuit::PlainText; } return format; } bool KuitFormatterPrivate::determineIsStructured(const QString &text, const KuitSetup &setup) { // If the text opens with a structuring tag, then it is structured, // otherwise not. Leading whitespace is ignored for this purpose. static const QRegularExpression opensWithTagRx(QStringLiteral("^\\s*<\\s*(\\w+)[^>]*>")); bool isStructured = false; const QRegularExpressionMatch match = opensWithTagRx.match(text); if (match.hasMatch()) { const QString tagName = match.captured(1).toLower(); if (setup.d->knownTags.contains(tagName)) { const KuitTag &tag = setup.d->knownTags.value(tagName); isStructured = (tag.type == Kuit::StructTag); } } return isStructured; } static const char s_entitySubRx[] = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+"; QString KuitFormatterPrivate::toVisualText(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const { KuitStaticData *s = staticData(); // Replace &-shortcut marker with "&", not to confuse the parser; // but do not touch & which forms an XML entity as it is. QString original = text_; // Regex is (see s_entitySubRx var): ^([a-z]+|#[0-9]+|#x[0-9a-fA-F]+); static const QRegularExpression restRx(QLatin1String("^(") + QLatin1String(s_entitySubRx) + QLatin1String(");")); QString text; int p = original.indexOf(QL1C('&')); while (p >= 0) { text.append(original.midRef(0, p + 1)); original.remove(0, p + 1); if (original.indexOf(restRx) != 0) { // not an entity text.append(QSL("amp;")); } p = original.indexOf(QL1C('&')); } text.append(original); // FIXME: Do this and then check proper use of structuring and phrase tags. #if 0 // Determine whether this is block-structured text. bool isStructured = determineIsStructured(text, setup); #endif const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__"); // Add top tag, not to confuse the parser. text = QStringLiteral("<%2>%1</%2>").arg(text, INTERNAL_TOP_TAG_NAME); QStack<OpenEl> openEls; QXmlStreamReader xml(text); xml.setEntityResolver(&s->xmlEntityResolver); QStringRef lastElementName; while (!xml.atEnd()) { xml.readNext(); if (xml.isStartElement()) { lastElementName = xml.name(); // Find first proper enclosing element. OpenEl enclosingOel; for (int i = openEls.size() - 1; i >= 0; --i) { if (openEls[i].handling == OpenEl::Proper) { enclosingOel = openEls[i]; break; } } // Collect data about this element. OpenEl oel = parseOpenEl(xml, enclosingOel, text, setup); // Record the new element on the parse stack. openEls.push(oel); } else if (xml.isEndElement()) { // Get closed element data. OpenEl oel = openEls.pop(); // If this was closing of the top element, we're done. if (openEls.isEmpty()) { // Return with final touches applied. return finalizeVisualText(oel.formattedText, format); } // Append formatted text segment. QString ptext = openEls.top().formattedText; // preceding text openEls.top().formattedText += formatSubText(ptext, oel, format, setup); } else if (xml.isCharacters()) { // Stream reader will automatically resolve default XML entities, // which is not desired in this case, as the entities are to be // resolved in finalizeVisualText. Convert back into entities. const QString ctext = xml.text().toString(); QString nctext; for (const QChar c : ctext) { if (s->xmlEntitiesInverse.contains(c)) { const QString entName = s->xmlEntitiesInverse[c]; nctext += QL1C('&') + entName + QL1C(';'); } else { nctext += c; } } openEls.top().formattedText += nctext; } } if (xml.hasError()) { qCWarning(KI18N_KUIT) << QStringLiteral( "Markup error in message {%1}: %2. Last tag parsed: %3. Complete message follows:\n%4") .arg(shorten(text), xml.errorString(), lastElementName.toString(), text); return QString(); } // Cannot reach here. return text; } KuitFormatterPrivate::OpenEl KuitFormatterPrivate::parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const { OpenEl oel; oel.name = xml.name().toString().toLower(); // Collect attribute names and values, and format attribute string. QStringList attribNames, attribValues; const auto listAttributes = xml.attributes(); for (const QXmlStreamAttribute &xatt : listAttributes) { attribNames += xatt.name().toString().toLower(); attribValues += xatt.value().toString(); QChar qc = attribValues.last().indexOf(QL1C('\'')) < 0 ? QL1C('\'') : QL1C('"'); oel.attribStr += QL1C(' ') + attribNames.last() + QL1C('=') + qc + attribValues.last() + qc; } if (setup.d->knownTags.contains(oel.name)) { // known KUIT element const KuitTag &tag = setup.d->knownTags.value(oel.name); const KuitTag &etag = setup.d->knownTags.value(enclosingOel.name); // If this element can be contained within enclosing element, // mark it proper, otherwise mark it for removal. if (tag.name.isEmpty() || tag.type == Kuit::PhraseTag || etag.type == Kuit::StructTag) { oel.handling = OpenEl::Proper; } else { oel.handling = OpenEl::Dropout; qCWarning(KI18N_KUIT) << QStringLiteral( "Structuring tag ('%1') cannot be subtag of phrase tag ('%2') in message {%3}.") .arg(tag.name, etag.name, shorten(text)); } // Resolve attributes and compute attribute set key. QSet<QString> attset; for (int i = 0; i < attribNames.size(); ++i) { QString att = attribNames[i]; if (tag.knownAttribs.contains(att)) { attset << att; oel.attributes[att] = attribValues[i]; } else { qCWarning(KI18N_KUIT) << QStringLiteral( "Attribute '%1' not defined for tag '%2' in message {%3}.") .arg(att, tag.name, shorten(text)); } } // Continue tag path. oel.tagPath = enclosingOel.tagPath; oel.tagPath.prepend(enclosingOel.name); } else { // unknown element, leave it in verbatim oel.handling = OpenEl::Ignored; qCWarning(KI18N_KUIT) << QStringLiteral( "Tag '%1' is not defined in message {%2}.") .arg(oel.name, shorten(text)); } return oel; } QString KuitFormatterPrivate::formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const { if (oel.handling == OpenEl::Proper) { const KuitTag &tag = setup.d->knownTags.value(oel.name); QString ftext = tag.format(languageAsList, oel.attributes, oel.formattedText, oel.tagPath, format); // Handle leading newlines, if this is not start of the text // (ptext is the preceding text). if (!ptext.isEmpty() && tag.leadingNewlines > 0) { // Count number of present newlines. int pnumle, pnumtr, fnumle, fnumtr; countWrappingNewlines(ptext, pnumle, pnumtr); countWrappingNewlines(ftext, fnumle, fnumtr); // Number of leading newlines already present. int numle = pnumtr + fnumle; // The required extra newlines. QString strle; if (numle < tag.leadingNewlines) { strle = QString(tag.leadingNewlines - numle, QL1C('\n')); } ftext = strle + ftext; } return ftext; } else if (oel.handling == OpenEl::Ignored) { return QL1C('<') + oel.name + oel.attribStr + QL1C('>') + oel.formattedText + QSL("</") + oel.name + QL1C('>'); } else { // oel.handling == OpenEl::Dropout return oel.formattedText; } } void KuitFormatterPrivate::countWrappingNewlines(const QString &text, int &numle, int &numtr) { int len = text.length(); // Number of newlines at start of text. numle = 0; while (numle < len && text[numle] == QL1C('\n')) { ++numle; } // Number of newlines at end of text. numtr = 0; while (numtr < len && text[len - numtr - 1] == QL1C('\n')) { ++numtr; } } QString KuitFormatterPrivate::finalizeVisualText(const QString &text_, Kuit::VisualFormat format) const { KuitStaticData *s = staticData(); QString text = text_; // Resolve XML entities. if (format != Kuit::RichText) { // regex is (see s_entitySubRx var): (&([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);) static const QRegularExpression entRx(QLatin1String("(&(") + QLatin1String(s_entitySubRx) + QLatin1String(");)")); QRegularExpressionMatch match; QString plain; while ((match = entRx.match(text)).hasMatch()) { const QString ent = match.captured(2); plain.append(text.midRef(0, match.capturedStart(0))); text.remove(0, match.capturedEnd(0)); if (ent.startsWith(QL1C('#'))) { // numeric character entity bool ok; const QChar c = ent.at(1) == QL1C('x') ? QChar(ent.midRef(2).toInt(&ok, 16)) : QChar(ent.midRef(1).toInt(&ok, 10)); if (ok) { plain.append(c); } else { // unknown Unicode point, leave as is plain.append(match.captured(0)); } } else if (s->xmlEntities.contains(ent)) { // known entity plain.append(s->xmlEntities[ent]); } else { // unknown entity, just leave as is plain.append(match.captured(0)); } } plain.append(text); text = plain; } // Add top tag. if (format == Kuit::RichText) { text = QLatin1String("<html>") + text + QLatin1String("</html>"); } return text; } QString KuitFormatterPrivate::salvageMarkup(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const { QString text = text_; QString ntext; - int pos; // Resolve tags simple-mindedly. // - tags with content - static QRegExp staticWrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)")); - QRegExp wrapRx = staticWrapRx; // QRegExp not thread-safe - wrapRx.setMinimal(true); - pos = 0; - //ntext.clear(); - while (true) { - int previousPos = pos; - pos = wrapRx.indexIn(text, previousPos); - if (pos < 0) { - ntext += text.midRef(previousPos); - break; - } - ntext += text.midRef(previousPos, pos - previousPos); - const QStringList capts = wrapRx.capturedTexts(); - QString tagname = capts[2].toLower(); - QString content = salvageMarkup(capts[4], format, setup); + static const QRegularExpression wrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)"), + QRegularExpression::InvertedGreedinessOption); + QRegularExpressionMatchIterator iter = wrapRx.globalMatch(text); + QRegularExpressionMatch match; + int pos = 0; + while (iter.hasNext()) { + match = iter.next(); + ntext += text.midRef(pos, match.capturedStart(0) - pos); + const QString tagname = match.captured(2).toLower(); + const QString content = salvageMarkup(match.captured(4), format, setup); if (setup.d->knownTags.contains(tagname)) { const KuitTag &tag = setup.d->knownTags.value(tagname); QHash<QString, QString> attributes; - // TODO: Do not ignore attributes (in capts[3]). + // TODO: Do not ignore attributes (in match.captured(3)). ntext += tag.format(languageAsList, attributes, content, QStringList(), format); } else { - ntext += capts[1] + content + capts[5]; + ntext += match.captured(1) + content + match.captured(5); } - pos += wrapRx.matchedLength(); + pos = match.capturedEnd(0); } + // get the remaining part after the last match in "text" + ntext += text.midRef(pos); text = ntext; // - tags without content - static QRegExp staticNowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>")); - QRegExp nowrRx = staticNowrRx; // QRegExp not thread-safe - nowrRx.setMinimal(true); + static const QRegularExpression nowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>"), + QRegularExpression::InvertedGreedinessOption); + iter = nowrRx.globalMatch(text); pos = 0; ntext.clear(); - while (true) { - int previousPos = pos; - pos = nowrRx.indexIn(text, previousPos); - if (pos < 0) { - ntext += text.midRef(previousPos); - break; - } - ntext += text.midRef(previousPos, pos - previousPos); - const QStringList capts = nowrRx.capturedTexts(); - QString tagname = capts[1].toLower(); + while (iter.hasNext()) { + match = iter.next(); + ntext += text.midRef(pos, match.capturedStart(0) - pos); + const QString tagname = match.captured(1).toLower(); if (setup.d->knownTags.contains(tagname)) { const KuitTag &tag = setup.d->knownTags.value(tagname); ntext += tag.format(languageAsList, QHash<QString, QString>(), QString(), QStringList(), format); } else { - ntext += capts[0]; + ntext += match.captured(0); } - pos += nowrRx.matchedLength(); + pos = match.capturedEnd(0); } + // get the remaining part after the last match in "text" + ntext += text.midRef(pos); text = ntext; // Add top tag. if (format == Kuit::RichText) { text = QStringLiteral("<html>") + text + QStringLiteral("</html>"); } return text; } KuitFormatter::KuitFormatter(const QString &language) : d(new KuitFormatterPrivate(language)) { } KuitFormatter::~KuitFormatter() { delete d; } QString KuitFormatter::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const { return d->format(domain, context, text, format); }