File indexing completed on 2024-05-12 03:55:39
0001 /* This file is part of the KDE libraries 0002 SPDX-FileCopyrightText: 2007, 2013 Chusslove Illich <caslav.ilic@gmx.net> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include <QDir> 0008 #include <QRegularExpression> 0009 #include <QSet> 0010 #include <QStack> 0011 #include <QXmlStreamReader> 0012 0013 #include <klazylocalizedstring.h> 0014 #include <klocalizedstring.h> 0015 #include <kuitsetup.h> 0016 #include <kuitsetup_p.h> 0017 0018 #include "ki18n_logging_kuit.h" 0019 0020 #define QL1S(x) QLatin1String(x) 0021 #define QSL(x) QStringLiteral(x) 0022 #define QL1C(x) QLatin1Char(x) 0023 0024 QString Kuit::escape(const QString &text) 0025 { 0026 int tlen = text.length(); 0027 QString ntext; 0028 ntext.reserve(tlen); 0029 for (int i = 0; i < tlen; ++i) { 0030 QChar c = text[i]; 0031 if (c == QL1C('&')) { 0032 ntext += QStringLiteral("&"); 0033 } else if (c == QL1C('<')) { 0034 ntext += QStringLiteral("<"); 0035 } else if (c == QL1C('>')) { 0036 ntext += QStringLiteral(">"); 0037 } else if (c == QL1C('\'')) { 0038 ntext += QStringLiteral("'"); 0039 } else if (c == QL1C('"')) { 0040 ntext += QStringLiteral("""); 0041 } else { 0042 ntext += c; 0043 } 0044 } 0045 0046 return ntext; 0047 } 0048 0049 // Truncates the string, for output of long messages. 0050 // (But don't truncate too much otherwise it's impossible to determine 0051 // which message is faulty if many messages have the same beginning). 0052 static QString shorten(const QString &str) 0053 { 0054 const int maxlen = 80; 0055 if (str.length() <= maxlen) { 0056 return str; 0057 } else { 0058 return QStringView(str).left(maxlen) + QSL("..."); 0059 } 0060 } 0061 0062 static void parseUiMarker(const QString &context_, QString &roleName, QString &cueName, QString &formatName) 0063 { 0064 // UI marker is in the form @role:cue/format, 0065 // and must start just after any leading whitespace in the context string. 0066 // Note that names remain untouched if the marker is not found. 0067 // Normalize the whole string, all lowercase. 0068 QString context = context_.trimmed().toLower(); 0069 if (context.startsWith(QL1C('@'))) { // found UI marker 0070 static const QRegularExpression wsRx(QStringLiteral("\\s")); 0071 context = context.mid(1, wsRx.match(context).capturedStart(0) - 1); 0072 0073 // Possible format. 0074 int pfmt = context.indexOf(QL1C('/')); 0075 if (pfmt >= 0) { 0076 formatName = context.mid(pfmt + 1); 0077 context.truncate(pfmt); 0078 } 0079 0080 // Possible subcue. 0081 int pcue = context.indexOf(QL1C(':')); 0082 if (pcue >= 0) { 0083 cueName = context.mid(pcue + 1); 0084 context.truncate(pcue); 0085 } 0086 0087 // Role. 0088 roleName = context; 0089 } 0090 } 0091 0092 // Custom entity resolver for QXmlStreamReader. 0093 class KuitEntityResolver : public QXmlStreamEntityResolver 0094 { 0095 public: 0096 void setEntities(const QHash<QString, QString> &entities) 0097 { 0098 entityMap = entities; 0099 } 0100 0101 QString resolveUndeclaredEntity(const QString &name) override 0102 { 0103 QString value = entityMap.value(name); 0104 // This will return empty string if the entity name is not known, 0105 // which will make QXmlStreamReader signal unknown entity error. 0106 return value; 0107 } 0108 0109 private: 0110 QHash<QString, QString> entityMap; 0111 }; 0112 0113 namespace Kuit 0114 { 0115 enum Role { // UI marker roles 0116 UndefinedRole, 0117 ActionRole, 0118 TitleRole, 0119 OptionRole, 0120 LabelRole, 0121 ItemRole, 0122 InfoRole, 0123 }; 0124 0125 enum Cue { // UI marker subcues 0126 UndefinedCue, 0127 ButtonCue, 0128 InmenuCue, 0129 IntoolbarCue, 0130 WindowCue, 0131 MenuCue, 0132 TabCue, 0133 GroupCue, 0134 ColumnCue, 0135 RowCue, 0136 SliderCue, 0137 SpinboxCue, 0138 ListboxCue, 0139 TextboxCue, 0140 ChooserCue, 0141 CheckCue, 0142 RadioCue, 0143 InlistboxCue, 0144 IntableCue, 0145 InrangeCue, 0146 IntextCue, 0147 ValuesuffixCue, 0148 TooltipCue, 0149 WhatsthisCue, 0150 PlaceholderCue, 0151 StatusCue, 0152 ProgressCue, 0153 TipofthedayCue, // deprecated in favor of UsagetipCue 0154 UsagetipCue, 0155 CreditCue, 0156 ShellCue, 0157 }; 0158 } 0159 0160 class KuitStaticData 0161 { 0162 public: 0163 QHash<QString, QString> xmlEntities; 0164 QHash<QString, QString> xmlEntitiesInverse; 0165 KuitEntityResolver xmlEntityResolver; 0166 0167 QHash<QString, Kuit::Role> rolesByName; 0168 QHash<QString, Kuit::Cue> cuesByName; 0169 QHash<QString, Kuit::VisualFormat> formatsByName; 0170 QHash<Kuit::VisualFormat, QString> namesByFormat; 0171 QHash<Kuit::Role, QSet<Kuit::Cue>> knownRoleCues; 0172 0173 QHash<Kuit::VisualFormat, KLocalizedString> comboKeyDelim; 0174 QHash<Kuit::VisualFormat, KLocalizedString> guiPathDelim; 0175 QHash<QString, KLocalizedString> keyNames; 0176 0177 QHash<QByteArray, KuitSetup *> domainSetups; 0178 0179 KuitStaticData(); 0180 ~KuitStaticData(); 0181 0182 KuitStaticData(const KuitStaticData &) = delete; 0183 KuitStaticData &operator=(const KuitStaticData &) = delete; 0184 0185 void setXmlEntityData(); 0186 0187 void setUiMarkerData(); 0188 0189 void setKeyName(const KLazyLocalizedString &keyName); 0190 void setTextTransformData(); 0191 QString toKeyCombo(const QStringList &languages, const QString &shstr, Kuit::VisualFormat format); 0192 QString toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format); 0193 }; 0194 0195 KuitStaticData::KuitStaticData() 0196 { 0197 setXmlEntityData(); 0198 setUiMarkerData(); 0199 setTextTransformData(); 0200 } 0201 0202 KuitStaticData::~KuitStaticData() 0203 { 0204 qDeleteAll(domainSetups); 0205 } 0206 0207 void KuitStaticData::setXmlEntityData() 0208 { 0209 QString LT = QStringLiteral("lt"); 0210 QString GT = QStringLiteral("gt"); 0211 QString AMP = QStringLiteral("amp"); 0212 QString APOS = QStringLiteral("apos"); 0213 QString QUOT = QStringLiteral("quot"); 0214 0215 // Default XML entities, direct and inverse mapping. 0216 xmlEntities[LT] = QString(QL1C('<')); 0217 xmlEntities[GT] = QString(QL1C('>')); 0218 xmlEntities[AMP] = QString(QL1C('&')); 0219 xmlEntities[APOS] = QString(QL1C('\'')); 0220 xmlEntities[QUOT] = QString(QL1C('"')); 0221 xmlEntitiesInverse[QString(QL1C('<'))] = LT; 0222 xmlEntitiesInverse[QString(QL1C('>'))] = GT; 0223 xmlEntitiesInverse[QString(QL1C('&'))] = AMP; 0224 xmlEntitiesInverse[QString(QL1C('\''))] = APOS; 0225 xmlEntitiesInverse[QString(QL1C('"'))] = QUOT; 0226 0227 // Custom XML entities. 0228 xmlEntities[QStringLiteral("nbsp")] = QString(QChar(0xa0)); 0229 0230 xmlEntityResolver.setEntities(xmlEntities); 0231 } 0232 // clang-format off 0233 void KuitStaticData::setUiMarkerData() 0234 { 0235 using namespace Kuit; 0236 0237 // Role names and their available subcues. 0238 #undef SET_ROLE 0239 #define SET_ROLE(role, name, cues) do { \ 0240 rolesByName[name] = role; \ 0241 knownRoleCues[role] << cues; \ 0242 } while (0) 0243 SET_ROLE(ActionRole, QStringLiteral("action"), 0244 ButtonCue << InmenuCue << IntoolbarCue); 0245 SET_ROLE(TitleRole, QStringLiteral("title"), 0246 WindowCue << MenuCue << TabCue << GroupCue 0247 << ColumnCue << RowCue); 0248 SET_ROLE(LabelRole, QStringLiteral("label"), 0249 SliderCue << SpinboxCue << ListboxCue << TextboxCue 0250 << ChooserCue); 0251 SET_ROLE(OptionRole, QStringLiteral("option"), 0252 CheckCue << RadioCue); 0253 SET_ROLE(ItemRole, QStringLiteral("item"), 0254 InmenuCue << InlistboxCue << IntableCue << InrangeCue 0255 << IntextCue << ValuesuffixCue); 0256 SET_ROLE(InfoRole, QStringLiteral("info"), 0257 TooltipCue << WhatsthisCue << PlaceholderCue << StatusCue << ProgressCue 0258 << TipofthedayCue << UsagetipCue << CreditCue << ShellCue); 0259 0260 // Cue names. 0261 #undef SET_CUE 0262 #define SET_CUE(cue, name) do { \ 0263 cuesByName[name] = cue; \ 0264 } while (0) 0265 SET_CUE(ButtonCue, QStringLiteral("button")); 0266 SET_CUE(InmenuCue, QStringLiteral("inmenu")); 0267 SET_CUE(IntoolbarCue, QStringLiteral("intoolbar")); 0268 SET_CUE(WindowCue, QStringLiteral("window")); 0269 SET_CUE(MenuCue, QStringLiteral("menu")); 0270 SET_CUE(TabCue, QStringLiteral("tab")); 0271 SET_CUE(GroupCue, QStringLiteral("group")); 0272 SET_CUE(ColumnCue, QStringLiteral("column")); 0273 SET_CUE(RowCue, QStringLiteral("row")); 0274 SET_CUE(SliderCue, QStringLiteral("slider")); 0275 SET_CUE(SpinboxCue, QStringLiteral("spinbox")); 0276 SET_CUE(ListboxCue, QStringLiteral("listbox")); 0277 SET_CUE(TextboxCue, QStringLiteral("textbox")); 0278 SET_CUE(ChooserCue, QStringLiteral("chooser")); 0279 SET_CUE(CheckCue, QStringLiteral("check")); 0280 SET_CUE(RadioCue, QStringLiteral("radio")); 0281 SET_CUE(InlistboxCue, QStringLiteral("inlistbox")); 0282 SET_CUE(IntableCue, QStringLiteral("intable")); 0283 SET_CUE(InrangeCue, QStringLiteral("inrange")); 0284 SET_CUE(IntextCue, QStringLiteral("intext")); 0285 SET_CUE(ValuesuffixCue, QStringLiteral("valuesuffix")); 0286 SET_CUE(TooltipCue, QStringLiteral("tooltip")); 0287 SET_CUE(WhatsthisCue, QStringLiteral("whatsthis")); 0288 SET_CUE(PlaceholderCue, QStringLiteral("placeholder")); 0289 SET_CUE(StatusCue, QStringLiteral("status")); 0290 SET_CUE(ProgressCue, QStringLiteral("progress")); 0291 SET_CUE(TipofthedayCue, QStringLiteral("tipoftheday")); 0292 SET_CUE(UsagetipCue, QStringLiteral("usagetip")); 0293 SET_CUE(CreditCue, QStringLiteral("credit")); 0294 SET_CUE(ShellCue, QStringLiteral("shell")); 0295 0296 // Format names. 0297 #undef SET_FORMAT 0298 #define SET_FORMAT(format, name) do { \ 0299 formatsByName[name] = format; \ 0300 namesByFormat[format] = name; \ 0301 } while (0) 0302 SET_FORMAT(UndefinedFormat, QStringLiteral("undefined")); 0303 SET_FORMAT(PlainText, QStringLiteral("plain")); 0304 SET_FORMAT(RichText, QStringLiteral("rich")); 0305 SET_FORMAT(TermText, QStringLiteral("term")); 0306 } 0307 0308 void KuitStaticData::setKeyName(const KLazyLocalizedString &keyName) 0309 { 0310 QString normname = QString::fromUtf8(keyName.untranslatedText()).trimmed().toLower(); 0311 keyNames[normname] = keyName; 0312 } 0313 0314 void KuitStaticData::setTextTransformData() 0315 { 0316 // i18n: Decide which string is used to delimit keys in a keyboard 0317 // shortcut (e.g. + in Ctrl+Alt+Tab) in plain text. 0318 comboKeyDelim[Kuit::PlainText] = ki18nc("shortcut-key-delimiter/plain", "+"); 0319 comboKeyDelim[Kuit::TermText] = comboKeyDelim[Kuit::PlainText]; 0320 // i18n: Decide which string is used to delimit keys in a keyboard 0321 // shortcut (e.g. + in Ctrl+Alt+Tab) in rich text. 0322 comboKeyDelim[Kuit::RichText] = ki18nc("shortcut-key-delimiter/rich", "+"); 0323 0324 // i18n: Decide which string is used to delimit elements in a GUI path 0325 // (e.g. -> in "Go to Settings->Advanced->Core tab.") in plain text. 0326 guiPathDelim[Kuit::PlainText] = ki18nc("gui-path-delimiter/plain", "→"); 0327 guiPathDelim[Kuit::TermText] = guiPathDelim[Kuit::PlainText]; 0328 // i18n: Decide which string is used to delimit elements in a GUI path 0329 // (e.g. -> in "Go to Settings->Advanced->Core tab.") in rich text. 0330 guiPathDelim[Kuit::RichText] = ki18nc("gui-path-delimiter/rich", "→"); 0331 // NOTE: The '→' glyph seems to be available in all widespread fonts. 0332 0333 // Collect keyboard key names. 0334 setKeyName(kli18nc("keyboard-key-name", "Alt")); 0335 setKeyName(kli18nc("keyboard-key-name", "AltGr")); 0336 setKeyName(kli18nc("keyboard-key-name", "Backspace")); 0337 setKeyName(kli18nc("keyboard-key-name", "CapsLock")); 0338 setKeyName(kli18nc("keyboard-key-name", "Control")); 0339 setKeyName(kli18nc("keyboard-key-name", "Ctrl")); 0340 setKeyName(kli18nc("keyboard-key-name", "Del")); 0341 setKeyName(kli18nc("keyboard-key-name", "Delete")); 0342 setKeyName(kli18nc("keyboard-key-name", "Down")); 0343 setKeyName(kli18nc("keyboard-key-name", "End")); 0344 setKeyName(kli18nc("keyboard-key-name", "Enter")); 0345 setKeyName(kli18nc("keyboard-key-name", "Esc")); 0346 setKeyName(kli18nc("keyboard-key-name", "Escape")); 0347 setKeyName(kli18nc("keyboard-key-name", "Home")); 0348 setKeyName(kli18nc("keyboard-key-name", "Hyper")); 0349 setKeyName(kli18nc("keyboard-key-name", "Ins")); 0350 setKeyName(kli18nc("keyboard-key-name", "Insert")); 0351 setKeyName(kli18nc("keyboard-key-name", "Left")); 0352 setKeyName(kli18nc("keyboard-key-name", "Menu")); 0353 setKeyName(kli18nc("keyboard-key-name", "Meta")); 0354 setKeyName(kli18nc("keyboard-key-name", "NumLock")); 0355 setKeyName(kli18nc("keyboard-key-name", "PageDown")); 0356 setKeyName(kli18nc("keyboard-key-name", "PageUp")); 0357 setKeyName(kli18nc("keyboard-key-name", "PgDown")); 0358 setKeyName(kli18nc("keyboard-key-name", "PgUp")); 0359 setKeyName(kli18nc("keyboard-key-name", "PauseBreak")); 0360 setKeyName(kli18nc("keyboard-key-name", "PrintScreen")); 0361 setKeyName(kli18nc("keyboard-key-name", "PrtScr")); 0362 setKeyName(kli18nc("keyboard-key-name", "Return")); 0363 setKeyName(kli18nc("keyboard-key-name", "Right")); 0364 setKeyName(kli18nc("keyboard-key-name", "ScrollLock")); 0365 setKeyName(kli18nc("keyboard-key-name", "Shift")); 0366 setKeyName(kli18nc("keyboard-key-name", "Space")); 0367 setKeyName(kli18nc("keyboard-key-name", "Super")); 0368 setKeyName(kli18nc("keyboard-key-name", "SysReq")); 0369 setKeyName(kli18nc("keyboard-key-name", "Tab")); 0370 setKeyName(kli18nc("keyboard-key-name", "Up")); 0371 setKeyName(kli18nc("keyboard-key-name", "Win")); 0372 setKeyName(kli18nc("keyboard-key-name", "F1")); 0373 setKeyName(kli18nc("keyboard-key-name", "F2")); 0374 setKeyName(kli18nc("keyboard-key-name", "F3")); 0375 setKeyName(kli18nc("keyboard-key-name", "F4")); 0376 setKeyName(kli18nc("keyboard-key-name", "F5")); 0377 setKeyName(kli18nc("keyboard-key-name", "F6")); 0378 setKeyName(kli18nc("keyboard-key-name", "F7")); 0379 setKeyName(kli18nc("keyboard-key-name", "F8")); 0380 setKeyName(kli18nc("keyboard-key-name", "F9")); 0381 setKeyName(kli18nc("keyboard-key-name", "F10")); 0382 setKeyName(kli18nc("keyboard-key-name", "F11")); 0383 setKeyName(kli18nc("keyboard-key-name", "F12")); 0384 // TODO: Add rest of the key names? 0385 } 0386 // clang-format on 0387 0388 QString KuitStaticData::toKeyCombo(const QStringList &languages, const QString &shstr, Kuit::VisualFormat format) 0389 { 0390 // Take '+' or '-' as input shortcut delimiter, 0391 // whichever is first encountered. 0392 static const QRegularExpression delimRx(QStringLiteral("[+-]")); 0393 0394 const QRegularExpressionMatch match = delimRx.match(shstr); 0395 QStringList keys; 0396 if (match.hasMatch()) { // delimiter found, multi-key shortcut 0397 const QString oldDelim = match.captured(0); 0398 keys = shstr.split(oldDelim, Qt::SkipEmptyParts); 0399 } else { // single-key shortcut, no delimiter found 0400 keys.append(shstr); 0401 } 0402 0403 for (QString &key : keys) { 0404 // Normalize key 0405 key = key.trimmed(); 0406 auto nameIt = keyNames.constFind(key.toLower()); 0407 if (nameIt != keyNames.constEnd()) { 0408 key = nameIt->toString(languages); 0409 } 0410 } 0411 const QString delim = comboKeyDelim.value(format).toString(languages); 0412 return keys.join(delim); 0413 } 0414 0415 QString KuitStaticData::toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format) 0416 { 0417 // Take '/', '|' or "->" as input path delimiter, 0418 // whichever is first encountered. 0419 static const QRegularExpression delimRx(QStringLiteral("\\||->")); 0420 const QRegularExpressionMatch match = delimRx.match(inpstr); 0421 if (match.hasMatch()) { // multi-element path 0422 const QString oldDelim = match.captured(0); 0423 QStringList guiels = inpstr.split(oldDelim, Qt::SkipEmptyParts); 0424 const QString delim = guiPathDelim.value(format).toString(languages); 0425 return guiels.join(delim); 0426 } 0427 0428 // single-element path, no delimiter found 0429 return inpstr; 0430 } 0431 0432 Q_GLOBAL_STATIC(KuitStaticData, staticData) 0433 0434 static QString attributeSetKey(const QStringList &attribNames_) 0435 { 0436 QStringList attribNames = attribNames_; 0437 std::sort(attribNames.begin(), attribNames.end()); 0438 QString key = QL1C('[') + attribNames.join(QL1C(' ')) + QL1C(']'); 0439 return key; 0440 } 0441 0442 class KuitTag 0443 { 0444 public: 0445 QString name; 0446 Kuit::TagClass type; 0447 QSet<QString> knownAttribs; 0448 QHash<QString, QHash<Kuit::VisualFormat, QStringList>> attributeOrders; 0449 QHash<QString, QHash<Kuit::VisualFormat, KLocalizedString>> patterns; 0450 QHash<QString, QHash<Kuit::VisualFormat, Kuit::TagFormatter>> formatters; 0451 int leadingNewlines; 0452 0453 KuitTag(const QString &_name, Kuit::TagClass _type) 0454 : name(_name) 0455 , type(_type) 0456 { 0457 } 0458 KuitTag() = default; 0459 0460 QString format(const QStringList &languages, 0461 const QHash<QString, QString> &attributes, 0462 const QString &text, 0463 const QStringList &tagPath, 0464 Kuit::VisualFormat format) const; 0465 }; 0466 0467 QString KuitTag::format(const QStringList &languages, 0468 const QHash<QString, QString> &attributes, 0469 const QString &text, 0470 const QStringList &tagPath, 0471 Kuit::VisualFormat format) const 0472 { 0473 KuitStaticData *s = staticData(); 0474 QString formattedText = text; 0475 QString attribKey = attributeSetKey(attributes.keys()); 0476 const QHash<Kuit::VisualFormat, KLocalizedString> pattern = patterns.value(attribKey); 0477 auto patternIt = pattern.constFind(format); 0478 if (patternIt != pattern.constEnd()) { 0479 QString modText; 0480 Kuit::TagFormatter formatter = formatters.value(attribKey).value(format); 0481 if (formatter != nullptr) { 0482 modText = formatter(languages, name, attributes, text, tagPath, format); 0483 } else { 0484 modText = text; 0485 } 0486 KLocalizedString aggText = *patternIt; 0487 // line below is first-aid fix.for e.g. <emphasis strong='true'>. 0488 // TODO: proper handling of boolean attributes still needed 0489 aggText = aggText.relaxSubs(); 0490 if (!aggText.isEmpty()) { 0491 aggText = aggText.subs(modText); 0492 const QStringList attributeOrder = attributeOrders.value(attribKey).value(format); 0493 for (const QString &attribName : attributeOrder) { 0494 aggText = aggText.subs(attributes.value(attribName)); 0495 } 0496 formattedText = aggText.ignoreMarkup().toString(languages); 0497 } else { 0498 formattedText = modText; 0499 } 0500 } else if (patterns.contains(attribKey)) { 0501 qCWarning(KI18N_KUIT) 0502 << QStringLiteral("Undefined visual format for tag <%1> and attribute combination %2: %3.").arg(name, attribKey, s->namesByFormat.value(format)); 0503 } else { 0504 qCWarning(KI18N_KUIT) << QStringLiteral("Undefined attribute combination for tag <%1>: %2.").arg(name, attribKey); 0505 } 0506 return formattedText; 0507 } 0508 0509 KuitSetup &Kuit::setupForDomain(const QByteArray &domain) 0510 { 0511 KuitStaticData *s = staticData(); 0512 KuitSetup *setup = s->domainSetups.value(domain); 0513 if (!setup) { 0514 setup = new KuitSetup(domain); 0515 s->domainSetups.insert(domain, setup); 0516 } 0517 return *setup; 0518 } 0519 0520 class KuitSetupPrivate 0521 { 0522 public: 0523 void setTagPattern(const QString &tagName, 0524 const QStringList &attribNames, 0525 Kuit::VisualFormat format, 0526 const KLocalizedString &pattern, 0527 Kuit::TagFormatter formatter, 0528 int leadingNewlines); 0529 0530 void setTagClass(const QString &tagName, Kuit::TagClass aClass); 0531 0532 void setFormatForMarker(const QString &marker, Kuit::VisualFormat format); 0533 0534 void setDefaultMarkup(); 0535 void setDefaultFormats(); 0536 0537 QByteArray domain; 0538 QHash<QString, KuitTag> knownTags; 0539 QHash<Kuit::Role, QHash<Kuit::Cue, Kuit::VisualFormat>> formatsByRoleCue; 0540 }; 0541 0542 void KuitSetupPrivate::setTagPattern(const QString &tagName, 0543 const QStringList &attribNames_, 0544 Kuit::VisualFormat format, 0545 const KLocalizedString &pattern, 0546 Kuit::TagFormatter formatter, 0547 int leadingNewlines_) 0548 { 0549 auto tagIt = knownTags.find(tagName); 0550 if (tagIt == knownTags.end()) { 0551 tagIt = knownTags.insert(tagName, KuitTag(tagName, Kuit::PhraseTag)); 0552 } 0553 0554 KuitTag &tag = *tagIt; 0555 0556 QStringList attribNames = attribNames_; 0557 attribNames.removeAll(QString()); 0558 for (const QString &attribName : std::as_const(attribNames)) { 0559 tag.knownAttribs.insert(attribName); 0560 } 0561 QString attribKey = attributeSetKey(attribNames); 0562 tag.attributeOrders[attribKey][format] = attribNames; 0563 tag.patterns[attribKey][format] = pattern; 0564 tag.formatters[attribKey][format] = formatter; 0565 tag.leadingNewlines = leadingNewlines_; 0566 } 0567 0568 void KuitSetupPrivate::setTagClass(const QString &tagName, Kuit::TagClass aClass) 0569 { 0570 auto tagIt = knownTags.find(tagName); 0571 if (tagIt == knownTags.end()) { 0572 knownTags.insert(tagName, KuitTag(tagName, aClass)); 0573 } else { 0574 tagIt->type = aClass; 0575 } 0576 } 0577 0578 void KuitSetupPrivate::setFormatForMarker(const QString &marker, Kuit::VisualFormat format) 0579 { 0580 KuitStaticData *s = staticData(); 0581 0582 QString roleName; 0583 QString cueName; 0584 QString formatName; 0585 parseUiMarker(marker, roleName, cueName, formatName); 0586 0587 Kuit::Role role; 0588 auto roleIt = s->rolesByName.constFind(roleName); 0589 if (roleIt != s->rolesByName.constEnd()) { 0590 role = *roleIt; 0591 } else if (!roleName.isEmpty()) { 0592 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker {%2}, visual format not set.").arg(roleName, marker); 0593 return; 0594 } else { 0595 qCWarning(KI18N_KUIT) << QStringLiteral("Empty role in UI marker {%1}, visual format not set.").arg(marker); 0596 return; 0597 } 0598 0599 Kuit::Cue cue; 0600 auto cueIt = s->cuesByName.constFind(cueName); 0601 if (cueIt != s->cuesByName.constEnd()) { 0602 cue = *cueIt; 0603 if (!s->knownRoleCues.value(role).contains(cue)) { 0604 qCWarning(KI18N_KUIT) 0605 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker {%3}, visual format not set.").arg(cueName, roleName, marker); 0606 return; 0607 } 0608 } else if (!cueName.isEmpty()) { 0609 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker {%2}, visual format not set.").arg(cueName, marker); 0610 return; 0611 } else { 0612 cue = Kuit::UndefinedCue; 0613 } 0614 0615 formatsByRoleCue[role][cue] = format; 0616 } 0617 0618 #define TAG_FORMATTER_ARGS \ 0619 const QStringList &languages, const QString &tagName, const QHash<QString, QString> &attributes, const QString &text, const QStringList &tagPath, \ 0620 Kuit::VisualFormat format 0621 0622 static QString tagFormatterFilename(TAG_FORMATTER_ARGS) 0623 { 0624 Q_UNUSED(languages); 0625 Q_UNUSED(tagName); 0626 Q_UNUSED(attributes); 0627 Q_UNUSED(tagPath); 0628 #ifdef Q_OS_WIN 0629 // with rich text the path can include <foo>...</foo> which will be replaced by <foo>...<\foo> on Windows! 0630 // the same problem also happens for tags such as <br/> -> <br\> 0631 if (format == Kuit::RichText) { 0632 // replace all occurrences of "</" or "/>" to make sure toNativeSeparators() doesn't destroy XML markup 0633 const auto KUIT_CLOSE_XML_REPLACEMENT = QStringLiteral("__kuit_close_xml_tag__"); 0634 const auto KUIT_NOTEXT_XML_REPLACEMENT = QStringLiteral("__kuit_notext_xml_tag__"); 0635 0636 QString result = text; 0637 result.replace(QStringLiteral("</"), KUIT_CLOSE_XML_REPLACEMENT); 0638 result.replace(QStringLiteral("/>"), KUIT_NOTEXT_XML_REPLACEMENT); 0639 result = QDir::toNativeSeparators(result); 0640 result.replace(KUIT_CLOSE_XML_REPLACEMENT, QStringLiteral("</")); 0641 result.replace(KUIT_NOTEXT_XML_REPLACEMENT, QStringLiteral("/>")); 0642 return result; 0643 } 0644 #else 0645 Q_UNUSED(format); 0646 #endif 0647 return QDir::toNativeSeparators(text); 0648 } 0649 0650 static QString tagFormatterShortcut(TAG_FORMATTER_ARGS) 0651 { 0652 Q_UNUSED(tagName); 0653 Q_UNUSED(attributes); 0654 Q_UNUSED(tagPath); 0655 KuitStaticData *s = staticData(); 0656 return s->toKeyCombo(languages, text, format); 0657 } 0658 0659 static QString tagFormatterInterface(TAG_FORMATTER_ARGS) 0660 { 0661 Q_UNUSED(tagName); 0662 Q_UNUSED(attributes); 0663 Q_UNUSED(tagPath); 0664 KuitStaticData *s = staticData(); 0665 return s->toInterfacePath(languages, text, format); 0666 } 0667 0668 void KuitSetupPrivate::setDefaultMarkup() 0669 { 0670 using namespace Kuit; 0671 0672 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__"); 0673 const QString TITLE = QStringLiteral("title"); 0674 const QString EMPHASIS = QStringLiteral("emphasis"); 0675 const QString COMMAND = QStringLiteral("command"); 0676 const QString WARNING = QStringLiteral("warning"); 0677 const QString LINK = QStringLiteral("link"); 0678 const QString NOTE = QStringLiteral("note"); 0679 0680 // clang-format off 0681 // Macro to hide message from extraction. 0682 #define HI18NC ki18nc 0683 0684 // Macro to expedite setting the patterns. 0685 #undef SET_PATTERN 0686 #define SET_PATTERN(tagName, attribNames_, format, pattern, formatter, leadNl) \ 0687 do { \ 0688 QStringList attribNames; \ 0689 attribNames << attribNames_; \ 0690 setTagPattern(tagName, attribNames, format, pattern, formatter, leadNl); \ 0691 /* Make TermText pattern same as PlainText if not explicitly given. */ \ 0692 KuitTag &tag = knownTags[tagName]; \ 0693 QString attribKey = attributeSetKey(attribNames); \ 0694 if (format == PlainText && !tag.patterns[attribKey].contains(TermText)) { \ 0695 setTagPattern(tagName, attribNames, TermText, pattern, formatter, leadNl); \ 0696 } \ 0697 } while (0) 0698 0699 // NOTE: The following "i18n:" comments are oddly placed in order that 0700 // xgettext extracts them properly. 0701 0702 // -------> Internal top tag 0703 setTagClass(INTERNAL_TOP_TAG_NAME, StructTag); 0704 SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), PlainText, 0705 HI18NC("tag-format-pattern <> plain", 0706 // i18n: KUIT pattern, see the comment to the first of these entries above. 0707 "%1"), 0708 nullptr, 0); 0709 SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), RichText, 0710 HI18NC("tag-format-pattern <> rich", 0711 // i18n: KUIT pattern, see the comment to the first of these entries above. 0712 "%1"), 0713 nullptr, 0); 0714 0715 // -------> Title 0716 setTagClass(TITLE, StructTag); 0717 SET_PATTERN(TITLE, QString(), PlainText, 0718 ki18nc("tag-format-pattern <title> plain", 0719 // i18n: The messages with context "tag-format-pattern <tag ...> format" 0720 // are KUIT patterns for formatting the text found inside KUIT tags. 0721 // The format is either "plain" or "rich", and tells if the pattern 0722 // is used for plain text or rich text (which can use HTML tags). 0723 // You may be in general satisfied with the patterns as they are in the 0724 // original. Some things you may consider changing: 0725 // - the proper quotes, those used in msgid are English-standard 0726 // - the <i> and <b> tags, does your language script work well with them? 0727 "== %1 =="), 0728 nullptr, 2); 0729 SET_PATTERN(TITLE, QString(), RichText, 0730 ki18nc("tag-format-pattern <title> rich", 0731 // i18n: KUIT pattern, see the comment to the first of these entries above. 0732 "<h2>%1</h2>"), 0733 nullptr, 2); 0734 0735 // -------> Subtitle 0736 setTagClass(QSL("subtitle"), StructTag); 0737 SET_PATTERN(QSL("subtitle"), QString(), PlainText, 0738 ki18nc("tag-format-pattern <subtitle> plain", 0739 // i18n: KUIT pattern, see the comment to the first of these entries above. 0740 "~ %1 ~"), 0741 nullptr, 2); 0742 SET_PATTERN(QSL("subtitle"), QString(), RichText, 0743 ki18nc("tag-format-pattern <subtitle> rich", 0744 // i18n: KUIT pattern, see the comment to the first of these entries above. 0745 "<h3>%1</h3>"), 0746 nullptr, 2); 0747 0748 // -------> Para 0749 setTagClass(QSL("para"), StructTag); 0750 SET_PATTERN(QSL("para"), QString(), PlainText, 0751 ki18nc("tag-format-pattern <para> plain", 0752 // i18n: KUIT pattern, see the comment to the first of these entries above. 0753 "%1"), 0754 nullptr, 2); 0755 SET_PATTERN(QSL("para"), QString(), RichText, 0756 ki18nc("tag-format-pattern <para> rich", 0757 // i18n: KUIT pattern, see the comment to the first of these entries above. 0758 "<p>%1</p>"), 0759 nullptr, 2); 0760 0761 // -------> List 0762 setTagClass(QSL("list"), StructTag); 0763 SET_PATTERN(QSL("list"), QString(), PlainText, 0764 ki18nc("tag-format-pattern <list> plain", 0765 // i18n: KUIT pattern, see the comment to the first of these entries above. 0766 "%1"), 0767 nullptr, 1); 0768 SET_PATTERN(QSL("list"), QString(), RichText, 0769 ki18nc("tag-format-pattern <list> rich", 0770 // i18n: KUIT pattern, see the comment to the first of these entries above. 0771 "<ul>%1</ul>"), 0772 nullptr, 1); 0773 0774 // -------> Item 0775 setTagClass(QSL("item"), StructTag); 0776 SET_PATTERN(QSL("item"), QString(), PlainText, 0777 ki18nc("tag-format-pattern <item> plain", 0778 // i18n: KUIT pattern, see the comment to the first of these entries above. 0779 " * %1"), 0780 nullptr, 1); 0781 SET_PATTERN(QSL("item"), QString(), RichText, 0782 ki18nc("tag-format-pattern <item> rich", 0783 // i18n: KUIT pattern, see the comment to the first of these entries above. 0784 "<li>%1</li>"), 0785 nullptr, 1); 0786 0787 // -------> Note 0788 SET_PATTERN(NOTE, QString(), PlainText, 0789 ki18nc("tag-format-pattern <note> plain", 0790 // i18n: KUIT pattern, see the comment to the first of these entries above. 0791 "Note: %1"), 0792 nullptr, 0); 0793 SET_PATTERN(NOTE, QString(), RichText, 0794 ki18nc("tag-format-pattern <note> rich", 0795 // i18n: KUIT pattern, see the comment to the first of these entries above. 0796 "<i>Note</i>: %1"), 0797 nullptr, 0); 0798 SET_PATTERN(NOTE, QSL("label"), PlainText, 0799 ki18nc("tag-format-pattern <note label=> plain\n" 0800 "%1 is the text, %2 is the note label", 0801 // i18n: KUIT pattern, see the comment to the first of these entries above. 0802 "%2: %1"), 0803 nullptr, 0); 0804 SET_PATTERN(NOTE, QSL("label"), RichText, 0805 ki18nc("tag-format-pattern <note label=> rich\n" 0806 "%1 is the text, %2 is the note label", 0807 // i18n: KUIT pattern, see the comment to the first of these entries above. 0808 "<i>%2</i>: %1"), 0809 nullptr, 0); 0810 0811 // -------> Warning 0812 SET_PATTERN(WARNING, QString(), PlainText, 0813 ki18nc("tag-format-pattern <warning> plain", 0814 // i18n: KUIT pattern, see the comment to the first of these entries above. 0815 "WARNING: %1"), 0816 nullptr, 0); 0817 SET_PATTERN(WARNING, QString(), RichText, 0818 ki18nc("tag-format-pattern <warning> rich", 0819 // i18n: KUIT pattern, see the comment to the first of these entries above. 0820 "<b>Warning</b>: %1"), 0821 nullptr, 0); 0822 SET_PATTERN(WARNING, QSL("label"), PlainText, 0823 ki18nc("tag-format-pattern <warning label=> plain\n" 0824 "%1 is the text, %2 is the warning label", 0825 // i18n: KUIT pattern, see the comment to the first of these entries above. 0826 "%2: %1"), 0827 nullptr, 0); 0828 SET_PATTERN(WARNING, QSL("label"), RichText, 0829 ki18nc("tag-format-pattern <warning label=> rich\n" 0830 "%1 is the text, %2 is the warning label", 0831 // i18n: KUIT pattern, see the comment to the first of these entries above. 0832 "<b>%2</b>: %1"), 0833 nullptr, 0); 0834 0835 // -------> Link 0836 SET_PATTERN(LINK, QString(), PlainText, 0837 ki18nc("tag-format-pattern <link> plain", 0838 // i18n: KUIT pattern, see the comment to the first of these entries above. 0839 "%1"), 0840 nullptr, 0); 0841 SET_PATTERN(LINK, QString(), RichText, 0842 ki18nc("tag-format-pattern <link> rich", 0843 // i18n: KUIT pattern, see the comment to the first of these entries above. 0844 "<a href=\"%1\">%1</a>"), 0845 nullptr, 0); 0846 SET_PATTERN(LINK, QSL("url"), PlainText, 0847 ki18nc("tag-format-pattern <link url=> plain\n" 0848 "%1 is the descriptive text, %2 is the URL", 0849 // i18n: KUIT pattern, see the comment to the first of these entries above. 0850 "%1 (%2)"), 0851 nullptr, 0); 0852 SET_PATTERN(LINK, QSL("url"), RichText, 0853 ki18nc("tag-format-pattern <link url=> rich\n" 0854 "%1 is the descriptive text, %2 is the URL", 0855 // i18n: KUIT pattern, see the comment to the first of these entries above. 0856 "<a href=\"%2\">%1</a>"), 0857 nullptr, 0); 0858 0859 // -------> Filename 0860 SET_PATTERN(QSL("filename"), QString(), PlainText, 0861 ki18nc("tag-format-pattern <filename> plain", 0862 // i18n: KUIT pattern, see the comment to the first of these entries above. 0863 "‘%1’"), 0864 tagFormatterFilename, 0); 0865 SET_PATTERN(QSL("filename"), QString(), RichText, 0866 ki18nc("tag-format-pattern <filename> rich", 0867 // i18n: KUIT pattern, see the comment to the first of these entries above. 0868 "‘<tt>%1</tt>’"), 0869 tagFormatterFilename, 0); 0870 0871 // -------> Application 0872 SET_PATTERN(QSL("application"), QString(), PlainText, 0873 ki18nc("tag-format-pattern <application> plain", 0874 // i18n: KUIT pattern, see the comment to the first of these entries above. 0875 "%1"), 0876 nullptr, 0); 0877 SET_PATTERN(QSL("application"), QString(), RichText, 0878 ki18nc("tag-format-pattern <application> rich", 0879 // i18n: KUIT pattern, see the comment to the first of these entries above. 0880 "%1"), 0881 nullptr, 0); 0882 0883 // -------> Command 0884 SET_PATTERN(COMMAND, QString(), PlainText, 0885 ki18nc("tag-format-pattern <command> plain", 0886 // i18n: KUIT pattern, see the comment to the first of these entries above. 0887 "%1"), 0888 nullptr, 0); 0889 SET_PATTERN(COMMAND, QString(), RichText, 0890 ki18nc("tag-format-pattern <command> rich", 0891 // i18n: KUIT pattern, see the comment to the first of these entries above. 0892 "<tt>%1</tt>"), 0893 nullptr, 0); 0894 SET_PATTERN(COMMAND, QSL("section"), PlainText, 0895 ki18nc("tag-format-pattern <command section=> plain\n" 0896 "%1 is the command name, %2 is its man section", 0897 // i18n: KUIT pattern, see the comment to the first of these entries above. 0898 "%1(%2)"), 0899 nullptr, 0); 0900 SET_PATTERN(COMMAND, QSL("section"), RichText, 0901 ki18nc("tag-format-pattern <command section=> rich\n" 0902 "%1 is the command name, %2 is its man section", 0903 // i18n: KUIT pattern, see the comment to the first of these entries above. 0904 "<tt>%1(%2)</tt>"), 0905 nullptr, 0); 0906 0907 // -------> Resource 0908 SET_PATTERN(QSL("resource"), QString(), PlainText, 0909 ki18nc("tag-format-pattern <resource> plain", 0910 // i18n: KUIT pattern, see the comment to the first of these entries above. 0911 "“%1”"), 0912 nullptr, 0); 0913 SET_PATTERN(QSL("resource"), QString(), RichText, 0914 ki18nc("tag-format-pattern <resource> rich", 0915 // i18n: KUIT pattern, see the comment to the first of these entries above. 0916 "“%1”"), 0917 nullptr, 0); 0918 0919 // -------> Icode 0920 SET_PATTERN(QSL("icode"), QString(), PlainText, 0921 ki18nc("tag-format-pattern <icode> plain", 0922 // i18n: KUIT pattern, see the comment to the first of these entries above. 0923 "“%1”"), 0924 nullptr, 0); 0925 SET_PATTERN(QSL("icode"), QString(), RichText, 0926 ki18nc("tag-format-pattern <icode> rich", 0927 // i18n: KUIT pattern, see the comment to the first of these entries above. 0928 "<tt>%1</tt>"), 0929 nullptr, 0); 0930 0931 // -------> Bcode 0932 SET_PATTERN(QSL("bcode"), QString(), PlainText, 0933 ki18nc("tag-format-pattern <bcode> plain", 0934 // i18n: KUIT pattern, see the comment to the first of these entries above. 0935 "\n%1\n"), 0936 nullptr, 2); 0937 SET_PATTERN(QSL("bcode"), QString(), RichText, 0938 ki18nc("tag-format-pattern <bcode> rich", 0939 // i18n: KUIT pattern, see the comment to the first of these entries above. 0940 "<pre>%1</pre>"), 0941 nullptr, 2); 0942 0943 // -------> Shortcut 0944 SET_PATTERN(QSL("shortcut"), QString(), PlainText, 0945 ki18nc("tag-format-pattern <shortcut> plain", 0946 // i18n: KUIT pattern, see the comment to the first of these entries above. 0947 "%1"), 0948 tagFormatterShortcut, 0); 0949 SET_PATTERN(QSL("shortcut"), QString(), RichText, 0950 ki18nc("tag-format-pattern <shortcut> rich", 0951 // i18n: KUIT pattern, see the comment to the first of these entries above. 0952 "<b>%1</b>"), 0953 tagFormatterShortcut, 0); 0954 0955 // -------> Interface 0956 SET_PATTERN(QSL("interface"), QString(), PlainText, 0957 ki18nc("tag-format-pattern <interface> plain", 0958 // i18n: KUIT pattern, see the comment to the first of these entries above. 0959 "|%1|"), 0960 tagFormatterInterface, 0); 0961 SET_PATTERN(QSL("interface"), QString(), RichText, 0962 ki18nc("tag-format-pattern <interface> rich", 0963 // i18n: KUIT pattern, see the comment to the first of these entries above. 0964 "<i>%1</i>"), 0965 tagFormatterInterface, 0); 0966 0967 // -------> Emphasis 0968 SET_PATTERN(EMPHASIS, QString(), PlainText, 0969 ki18nc("tag-format-pattern <emphasis> plain", 0970 // i18n: KUIT pattern, see the comment to the first of these entries above. 0971 "*%1*"), 0972 nullptr, 0); 0973 SET_PATTERN(EMPHASIS, QString(), RichText, 0974 ki18nc("tag-format-pattern <emphasis> rich", 0975 // i18n: KUIT pattern, see the comment to the first of these entries above. 0976 "<i>%1</i>"), 0977 nullptr, 0); 0978 SET_PATTERN(EMPHASIS, QSL("strong"), PlainText, 0979 ki18nc("tag-format-pattern <emphasis-strong> plain", 0980 // i18n: KUIT pattern, see the comment to the first of these entries above. 0981 "**%1**"), 0982 nullptr, 0); 0983 SET_PATTERN(EMPHASIS, QSL("strong"), RichText, 0984 ki18nc("tag-format-pattern <emphasis-strong> rich", 0985 // i18n: KUIT pattern, see the comment to the first of these entries above. 0986 "<b>%1</b>"), 0987 nullptr, 0); 0988 0989 // -------> Placeholder 0990 SET_PATTERN(QSL("placeholder"), QString(), PlainText, 0991 ki18nc("tag-format-pattern <placeholder> plain", 0992 // i18n: KUIT pattern, see the comment to the first of these entries above. 0993 "<%1>"), 0994 nullptr, 0); 0995 SET_PATTERN(QSL("placeholder"), QString(), RichText, 0996 ki18nc("tag-format-pattern <placeholder> rich", 0997 // i18n: KUIT pattern, see the comment to the first of these entries above. 0998 "<<i>%1</i>>"), 0999 nullptr, 0); 1000 1001 // -------> Email 1002 SET_PATTERN(QSL("email"), QString(), PlainText, 1003 ki18nc("tag-format-pattern <email> plain", 1004 // i18n: KUIT pattern, see the comment to the first of these entries above. 1005 "<%1>"), 1006 nullptr, 0); 1007 SET_PATTERN(QSL("email"), QString(), RichText, 1008 ki18nc("tag-format-pattern <email> rich", 1009 // i18n: KUIT pattern, see the comment to the first of these entries above. 1010 "<<a href=\"mailto:%1\">%1</a>>"), 1011 nullptr, 0); 1012 SET_PATTERN(QSL("email"), QSL("address"), PlainText, 1013 ki18nc("tag-format-pattern <email address=> plain\n" 1014 "%1 is name, %2 is address", 1015 // i18n: KUIT pattern, see the comment to the first of these entries above. 1016 "%1 <%2>"), 1017 nullptr, 0); 1018 SET_PATTERN(QSL("email"), QSL("address"), RichText, 1019 ki18nc("tag-format-pattern <email address=> rich\n" 1020 "%1 is name, %2 is address", 1021 // i18n: KUIT pattern, see the comment to the first of these entries above. 1022 "<a href=\"mailto:%2\">%1</a>"), 1023 nullptr, 0); 1024 1025 // -------> Envar 1026 SET_PATTERN(QSL("envar"), QString(), PlainText, 1027 ki18nc("tag-format-pattern <envar> plain", 1028 // i18n: KUIT pattern, see the comment to the first of these entries above. 1029 "$%1"), 1030 nullptr, 0); 1031 SET_PATTERN(QSL("envar"), QString(), RichText, 1032 ki18nc("tag-format-pattern <envar> rich", 1033 // i18n: KUIT pattern, see the comment to the first of these entries above. 1034 "<tt>$%1</tt>"), 1035 nullptr, 0); 1036 1037 // -------> Message 1038 SET_PATTERN(QSL("message"), QString(), PlainText, 1039 ki18nc("tag-format-pattern <message> plain", 1040 // i18n: KUIT pattern, see the comment to the first of these entries above. 1041 "/%1/"), 1042 nullptr, 0); 1043 SET_PATTERN(QSL("message"), QString(), RichText, 1044 ki18nc("tag-format-pattern <message> rich", 1045 // i18n: KUIT pattern, see the comment to the first of these entries above. 1046 "<i>%1</i>"), 1047 nullptr, 0); 1048 1049 // -------> Nl 1050 SET_PATTERN(QSL("nl"), QString(), PlainText, 1051 ki18nc("tag-format-pattern <nl> plain", 1052 // i18n: KUIT pattern, see the comment to the first of these entries above. 1053 "%1\n"), 1054 nullptr, 0); 1055 SET_PATTERN(QSL("nl"), QString(), RichText, 1056 ki18nc("tag-format-pattern <nl> rich", 1057 // i18n: KUIT pattern, see the comment to the first of these entries above. 1058 "%1<br/>"), 1059 nullptr, 0); 1060 // clang-format on 1061 } 1062 1063 void KuitSetupPrivate::setDefaultFormats() 1064 { 1065 using namespace Kuit; 1066 1067 // Setup formats by role. 1068 formatsByRoleCue[ActionRole][UndefinedCue] = PlainText; 1069 formatsByRoleCue[TitleRole][UndefinedCue] = PlainText; 1070 formatsByRoleCue[LabelRole][UndefinedCue] = PlainText; 1071 formatsByRoleCue[OptionRole][UndefinedCue] = PlainText; 1072 formatsByRoleCue[ItemRole][UndefinedCue] = PlainText; 1073 formatsByRoleCue[InfoRole][UndefinedCue] = RichText; 1074 1075 // Setup override formats by subcue. 1076 formatsByRoleCue[InfoRole][StatusCue] = PlainText; 1077 formatsByRoleCue[InfoRole][ProgressCue] = PlainText; 1078 formatsByRoleCue[InfoRole][CreditCue] = PlainText; 1079 formatsByRoleCue[InfoRole][ShellCue] = TermText; 1080 } 1081 1082 KuitSetup::KuitSetup(const QByteArray &domain) 1083 : d(new KuitSetupPrivate) 1084 { 1085 d->domain = domain; 1086 d->setDefaultMarkup(); 1087 d->setDefaultFormats(); 1088 } 1089 1090 KuitSetup::~KuitSetup() = default; 1091 1092 void KuitSetup::setTagPattern(const QString &tagName, 1093 const QStringList &attribNames, 1094 Kuit::VisualFormat format, 1095 const KLocalizedString &pattern, 1096 Kuit::TagFormatter formatter, 1097 int leadingNewlines) 1098 { 1099 d->setTagPattern(tagName, attribNames, format, pattern, formatter, leadingNewlines); 1100 } 1101 1102 void KuitSetup::setTagClass(const QString &tagName, Kuit::TagClass aClass) 1103 { 1104 d->setTagClass(tagName, aClass); 1105 } 1106 1107 void KuitSetup::setFormatForMarker(const QString &marker, Kuit::VisualFormat format) 1108 { 1109 d->setFormatForMarker(marker, format); 1110 } 1111 1112 class KuitFormatterPrivate 1113 { 1114 public: 1115 KuitFormatterPrivate(const QString &language); 1116 1117 QString format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const; 1118 1119 // Get metatranslation (formatting patterns, etc.) 1120 QString metaTr(const char *context, const char *text) const; 1121 1122 // Set visual formatting patterns for text within tags. 1123 void setFormattingPatterns(); 1124 1125 // Set data used in transformation of text within tags. 1126 void setTextTransformData(); 1127 1128 // Determine visual format by parsing the UI marker in the context. 1129 static Kuit::VisualFormat formatFromUiMarker(const QString &context, const KuitSetup &setup); 1130 1131 // Determine if text has block structure (multiple paragraphs, etc). 1132 static bool determineIsStructured(const QString &text, const KuitSetup &setup); 1133 1134 // Format KUIT text into visual text. 1135 QString toVisualText(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const; 1136 1137 // Final touches to the formatted text. 1138 QString finalizeVisualText(const QString &ftext, Kuit::VisualFormat format) const; 1139 1140 // In case of markup errors, try to make result not look too bad. 1141 QString salvageMarkup(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const; 1142 1143 // Data for XML parsing state. 1144 class OpenEl 1145 { 1146 public: 1147 enum Handling { Proper, Ignored, Dropout }; 1148 1149 QString name; 1150 QHash<QString, QString> attributes; 1151 QString attribStr; 1152 Handling handling; 1153 QString formattedText; 1154 QStringList tagPath; 1155 }; 1156 1157 // Gather data about current element for the parse state. 1158 KuitFormatterPrivate::OpenEl parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const; 1159 1160 // Format text of the element. 1161 QString formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const; 1162 1163 // Count number of newlines at start and at end of text. 1164 static void countWrappingNewlines(const QString &ptext, int &numle, int &numtr); 1165 1166 private: 1167 QString language; 1168 QStringList languageAsList; 1169 1170 QHash<Kuit::VisualFormat, QString> comboKeyDelim; 1171 QHash<Kuit::VisualFormat, QString> guiPathDelim; 1172 1173 QHash<QString, QString> keyNames; 1174 }; 1175 1176 KuitFormatterPrivate::KuitFormatterPrivate(const QString &language_) 1177 : language(language_) 1178 { 1179 } 1180 1181 QString KuitFormatterPrivate::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const 1182 { 1183 const KuitSetup &setup = Kuit::setupForDomain(domain); 1184 1185 // If format is undefined, determine it based on UI marker inside context. 1186 Kuit::VisualFormat resolvedFormat = format; 1187 if (resolvedFormat == Kuit::UndefinedFormat) { 1188 resolvedFormat = formatFromUiMarker(context, setup); 1189 } 1190 1191 // Quick check: are there any tags at all? 1192 QString ftext; 1193 if (text.indexOf(QL1C('<')) < 0) { 1194 ftext = finalizeVisualText(text, resolvedFormat); 1195 } else { 1196 // Format the text. 1197 ftext = toVisualText(text, resolvedFormat, setup); 1198 if (ftext.isEmpty()) { // error while processing markup 1199 ftext = salvageMarkup(text, resolvedFormat, setup); 1200 } 1201 } 1202 return ftext; 1203 } 1204 1205 Kuit::VisualFormat KuitFormatterPrivate::formatFromUiMarker(const QString &context, const KuitSetup &setup) 1206 { 1207 KuitStaticData *s = staticData(); 1208 1209 QString roleName; 1210 QString cueName; 1211 QString formatName; 1212 parseUiMarker(context, roleName, cueName, formatName); 1213 1214 // Set role from name. 1215 Kuit::Role role = s->rolesByName.value(roleName, Kuit::UndefinedRole); 1216 if (role == Kuit::UndefinedRole) { // unknown role 1217 if (!roleName.isEmpty()) { 1218 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker in context {%2}.").arg(roleName, shorten(context)); 1219 } 1220 } 1221 1222 // Set subcue from name. 1223 Kuit::Cue cue; 1224 if (role != Kuit::UndefinedRole) { 1225 cue = s->cuesByName.value(cueName, Kuit::UndefinedCue); 1226 if (cue != Kuit::UndefinedCue) { // known subcue 1227 if (!s->knownRoleCues.value(role).contains(cue)) { 1228 cue = Kuit::UndefinedCue; 1229 qCWarning(KI18N_KUIT) 1230 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker in context {%3}.").arg(cueName, roleName, shorten(context)); 1231 } 1232 } else { // unknown or not given subcue 1233 if (!cueName.isEmpty()) { 1234 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker in context {%2}.").arg(cueName, shorten(context)); 1235 } 1236 } 1237 } else { 1238 // Bad role, silently ignore the cue. 1239 cue = Kuit::UndefinedCue; 1240 } 1241 1242 // Set format from name, or by derivation from context/subcue. 1243 Kuit::VisualFormat format = s->formatsByName.value(formatName, Kuit::UndefinedFormat); 1244 if (format == Kuit::UndefinedFormat) { // unknown or not given format 1245 // Check first if there is a format defined for role/subcue 1246 // combination, then for role only, otherwise default to undefined. 1247 auto formatsByCueIt = setup.d->formatsByRoleCue.constFind(role); 1248 if (formatsByCueIt != setup.d->formatsByRoleCue.constEnd()) { 1249 const auto &formatsByCue = *formatsByCueIt; 1250 auto formatIt = formatsByCue.constFind(cue); 1251 if (formatIt != formatsByCue.constEnd()) { 1252 format = *formatIt; 1253 } else { 1254 format = formatsByCue.value(Kuit::UndefinedCue); 1255 } 1256 } 1257 if (!formatName.isEmpty()) { 1258 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown format '/%1' in UI marker for message {%2}.").arg(formatName, shorten(context)); 1259 } 1260 } 1261 if (format == Kuit::UndefinedFormat) { 1262 format = Kuit::PlainText; 1263 } 1264 1265 return format; 1266 } 1267 1268 bool KuitFormatterPrivate::determineIsStructured(const QString &text, const KuitSetup &setup) 1269 { 1270 // If the text opens with a structuring tag, then it is structured, 1271 // otherwise not. Leading whitespace is ignored for this purpose. 1272 static const QRegularExpression opensWithTagRx(QStringLiteral("^\\s*<\\s*(\\w+)[^>]*>")); 1273 bool isStructured = false; 1274 const QRegularExpressionMatch match = opensWithTagRx.match(text); 1275 if (match.hasMatch()) { 1276 const QString tagName = match.captured(1).toLower(); 1277 auto tagIt = setup.d->knownTags.constFind(tagName); 1278 if (tagIt != setup.d->knownTags.constEnd()) { 1279 const KuitTag &tag = *tagIt; 1280 isStructured = (tag.type == Kuit::StructTag); 1281 } 1282 } 1283 return isStructured; 1284 } 1285 1286 static const char s_entitySubRx[] = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+"; 1287 1288 QString KuitFormatterPrivate::toVisualText(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const 1289 { 1290 KuitStaticData *s = staticData(); 1291 1292 // Replace &-shortcut marker with "&", not to confuse the parser; 1293 // but do not touch & which forms an XML entity as it is. 1294 QString original = text_; 1295 // Regex is (see s_entitySubRx var): ^([a-z]+|#[0-9]+|#x[0-9a-fA-F]+); 1296 static const QRegularExpression restRx(QLatin1String("^(") + QLatin1String(s_entitySubRx) + QLatin1String(");")); 1297 1298 QString text; 1299 int p = original.indexOf(QL1C('&')); 1300 while (p >= 0) { 1301 text.append(QStringView(original).mid(0, p + 1)); 1302 original.remove(0, p + 1); 1303 if (original.indexOf(restRx) != 0) { // not an entity 1304 text.append(QSL("amp;")); 1305 } 1306 p = original.indexOf(QL1C('&')); 1307 } 1308 text.append(original); 1309 1310 // FIXME: Do this and then check proper use of structuring and phrase tags. 1311 #if 0 1312 // Determine whether this is block-structured text. 1313 bool isStructured = determineIsStructured(text, setup); 1314 #endif 1315 1316 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__"); 1317 // Add top tag, not to confuse the parser. 1318 text = QStringLiteral("<%2>%1</%2>").arg(text, INTERNAL_TOP_TAG_NAME); 1319 1320 QStack<OpenEl> openEls; 1321 QXmlStreamReader xml(text); 1322 xml.setEntityResolver(&s->xmlEntityResolver); 1323 QStringView lastElementName; 1324 1325 while (!xml.atEnd()) { 1326 xml.readNext(); 1327 1328 if (xml.isStartElement()) { 1329 lastElementName = xml.name(); 1330 1331 OpenEl oel; 1332 1333 if (openEls.isEmpty()) { 1334 // Must be the root element. 1335 oel.name = INTERNAL_TOP_TAG_NAME; 1336 oel.handling = OpenEl::Proper; 1337 } else { 1338 // Find first proper enclosing element. 1339 OpenEl enclosingOel; 1340 for (int i = openEls.size() - 1; i >= 0; --i) { 1341 if (openEls[i].handling == OpenEl::Proper) { 1342 enclosingOel = openEls[i]; 1343 break; 1344 } 1345 } 1346 // Collect data about this element. 1347 oel = parseOpenEl(xml, enclosingOel, text, setup); 1348 } 1349 1350 // Record the new element on the parse stack. 1351 openEls.push(oel); 1352 } else if (xml.isEndElement()) { 1353 // Get closed element data. 1354 OpenEl oel = openEls.pop(); 1355 1356 // If this was closing of the top element, we're done. 1357 if (openEls.isEmpty()) { 1358 // Return with final touches applied. 1359 return finalizeVisualText(oel.formattedText, format); 1360 } 1361 1362 // Append formatted text segment. 1363 QString ptext = openEls.top().formattedText; // preceding text 1364 openEls.top().formattedText += formatSubText(ptext, oel, format, setup); 1365 } else if (xml.isCharacters()) { 1366 // Stream reader will automatically resolve default XML entities, 1367 // which is not desired in this case, as the entities are to be 1368 // resolved in finalizeVisualText. Convert back into entities. 1369 const QString ctext = xml.text().toString(); 1370 QString nctext; 1371 for (const QChar c : ctext) { 1372 auto nameIt = s->xmlEntitiesInverse.constFind(c); 1373 if (nameIt != s->xmlEntitiesInverse.constEnd()) { 1374 const QString &entName = *nameIt; 1375 nctext += QL1C('&') + entName + QL1C(';'); 1376 } else { 1377 nctext += c; 1378 } 1379 } 1380 openEls.top().formattedText += nctext; 1381 } 1382 } 1383 1384 if (xml.hasError()) { 1385 qCWarning(KI18N_KUIT) << QStringLiteral("Markup error in message {%1}: %2. Last tag parsed: %3. Complete message follows:\n%4") 1386 .arg(shorten(text), xml.errorString(), lastElementName.toString(), text); 1387 return QString(); 1388 } 1389 1390 // Cannot reach here. 1391 return text; 1392 } 1393 1394 KuitFormatterPrivate::OpenEl 1395 KuitFormatterPrivate::parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const 1396 { 1397 OpenEl oel; 1398 oel.name = xml.name().toString().toLower(); 1399 1400 // Collect attribute names and values, and format attribute string. 1401 QStringList attribNames; 1402 QStringList attribValues; 1403 const auto listAttributes = xml.attributes(); 1404 attribNames.reserve(listAttributes.size()); 1405 attribValues.reserve(listAttributes.size()); 1406 for (const QXmlStreamAttribute &xatt : listAttributes) { 1407 attribNames += xatt.name().toString().toLower(); 1408 attribValues += xatt.value().toString(); 1409 QChar qc = attribValues.last().indexOf(QL1C('\'')) < 0 ? QL1C('\'') : QL1C('"'); 1410 oel.attribStr += QL1C(' ') + attribNames.last() + QL1C('=') + qc + attribValues.last() + qc; 1411 } 1412 1413 auto tagIt = setup.d->knownTags.constFind(oel.name); 1414 if (tagIt != setup.d->knownTags.constEnd()) { // known KUIT element 1415 const KuitTag &tag = *tagIt; 1416 const KuitTag &etag = setup.d->knownTags.value(enclosingOel.name); 1417 1418 // If this element can be contained within enclosing element, 1419 // mark it proper, otherwise mark it for removal. 1420 if (tag.name.isEmpty() || tag.type == Kuit::PhraseTag || etag.type == Kuit::StructTag) { 1421 oel.handling = OpenEl::Proper; 1422 } else { 1423 oel.handling = OpenEl::Dropout; 1424 qCWarning(KI18N_KUIT) 1425 << QStringLiteral("Structuring tag ('%1') cannot be subtag of phrase tag ('%2') in message {%3}.").arg(tag.name, etag.name, shorten(text)); 1426 } 1427 1428 // Resolve attributes and compute attribute set key. 1429 QSet<QString> attset; 1430 for (int i = 0; i < attribNames.size(); ++i) { 1431 QString att = attribNames[i]; 1432 if (tag.knownAttribs.contains(att)) { 1433 attset << att; 1434 oel.attributes[att] = attribValues[i]; 1435 } else { 1436 qCWarning(KI18N_KUIT) << QStringLiteral("Attribute '%1' not defined for tag '%2' in message {%3}.").arg(att, tag.name, shorten(text)); 1437 } 1438 } 1439 1440 // Continue tag path. 1441 oel.tagPath = enclosingOel.tagPath; 1442 oel.tagPath.prepend(enclosingOel.name); 1443 1444 } else { // unknown element, leave it in verbatim 1445 oel.handling = OpenEl::Ignored; 1446 qCWarning(KI18N_KUIT) << QStringLiteral("Tag '%1' is not defined in message {%2}.").arg(oel.name, shorten(text)); 1447 } 1448 1449 return oel; 1450 } 1451 1452 QString KuitFormatterPrivate::formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const 1453 { 1454 if (oel.handling == OpenEl::Proper) { 1455 const KuitTag &tag = setup.d->knownTags.value(oel.name); 1456 QString ftext = tag.format(languageAsList, oel.attributes, oel.formattedText, oel.tagPath, format); 1457 1458 // Handle leading newlines, if this is not start of the text 1459 // (ptext is the preceding text). 1460 if (!ptext.isEmpty() && tag.leadingNewlines > 0) { 1461 // Count number of present newlines. 1462 int pnumle; 1463 int pnumtr; 1464 int fnumle; 1465 int fnumtr; 1466 countWrappingNewlines(ptext, pnumle, pnumtr); 1467 countWrappingNewlines(ftext, fnumle, fnumtr); 1468 // Number of leading newlines already present. 1469 int numle = pnumtr + fnumle; 1470 // The required extra newlines. 1471 QString strle; 1472 if (numle < tag.leadingNewlines) { 1473 strle = QString(tag.leadingNewlines - numle, QL1C('\n')); 1474 } 1475 ftext = strle + ftext; 1476 } 1477 1478 return ftext; 1479 1480 } else if (oel.handling == OpenEl::Ignored) { 1481 return QL1C('<') + oel.name + oel.attribStr + QL1C('>') + oel.formattedText + QSL("</") + oel.name + QL1C('>'); 1482 1483 } else { // oel.handling == OpenEl::Dropout 1484 return oel.formattedText; 1485 } 1486 } 1487 1488 void KuitFormatterPrivate::countWrappingNewlines(const QString &text, int &numle, int &numtr) 1489 { 1490 int len = text.length(); 1491 // Number of newlines at start of text. 1492 numle = 0; 1493 while (numle < len && text[numle] == QL1C('\n')) { 1494 ++numle; 1495 } 1496 // Number of newlines at end of text. 1497 numtr = 0; 1498 while (numtr < len && text[len - numtr - 1] == QL1C('\n')) { 1499 ++numtr; 1500 } 1501 } 1502 1503 QString KuitFormatterPrivate::finalizeVisualText(const QString &text_, Kuit::VisualFormat format) const 1504 { 1505 KuitStaticData *s = staticData(); 1506 1507 QString text = text_; 1508 1509 // Resolve XML entities. 1510 if (format != Kuit::RichText) { 1511 // regex is (see s_entitySubRx var): (&([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);) 1512 static const QRegularExpression entRx(QLatin1String("(&(") + QLatin1String(s_entitySubRx) + QLatin1String(");)")); 1513 QRegularExpressionMatch match; 1514 QString plain; 1515 while ((match = entRx.match(text)).hasMatch()) { 1516 plain.append(QStringView(text).mid(0, match.capturedStart(0))); 1517 text.remove(0, match.capturedEnd(0)); 1518 const QString ent = match.captured(2); 1519 if (ent.startsWith(QL1C('#'))) { // numeric character entity 1520 bool ok; 1521 QStringView entView(ent); 1522 const QChar c = ent.at(1) == QL1C('x') ? QChar(entView.mid(2).toInt(&ok, 16)) : QChar(entView.mid(1).toInt(&ok, 10)); 1523 if (ok) { 1524 plain.append(c); 1525 } else { // unknown Unicode point, leave as is 1526 plain.append(match.capturedView(0)); 1527 } 1528 } else if (s->xmlEntities.contains(ent)) { // known entity 1529 plain.append(s->xmlEntities[ent]); 1530 } else { // unknown entity, just leave as is 1531 plain.append(match.capturedView(0)); 1532 } 1533 } 1534 plain.append(text); 1535 text = plain; 1536 } 1537 1538 // Add top tag. 1539 if (format == Kuit::RichText) { 1540 text = QLatin1String("<html>") + text + QLatin1String("</html>"); 1541 } 1542 1543 return text; 1544 } 1545 1546 QString KuitFormatterPrivate::salvageMarkup(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const 1547 { 1548 QString text = text_; 1549 QString ntext; 1550 1551 // Resolve tags simple-mindedly. 1552 1553 // - tags with content 1554 static const QRegularExpression wrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)"), QRegularExpression::InvertedGreedinessOption); 1555 QRegularExpressionMatchIterator iter = wrapRx.globalMatch(text); 1556 QRegularExpressionMatch match; 1557 int pos = 0; 1558 while (iter.hasNext()) { 1559 match = iter.next(); 1560 ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos); 1561 const QString tagname = match.captured(2).toLower(); 1562 const QString content = salvageMarkup(match.captured(4), format, setup); 1563 auto tagIt = setup.d->knownTags.constFind(tagname); 1564 if (tagIt != setup.d->knownTags.constEnd()) { 1565 const KuitTag &tag = *tagIt; 1566 QHash<QString, QString> attributes; 1567 // TODO: Do not ignore attributes (in match.captured(3)). 1568 ntext += tag.format(languageAsList, attributes, content, QStringList(), format); 1569 } else { 1570 ntext += match.captured(1) + content + match.captured(5); 1571 } 1572 pos = match.capturedEnd(0); 1573 } 1574 // get the remaining part after the last match in "text" 1575 ntext += QStringView(text).mid(pos); 1576 text = ntext; 1577 1578 // - tags without content 1579 static const QRegularExpression nowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>"), QRegularExpression::InvertedGreedinessOption); 1580 iter = nowrRx.globalMatch(text); 1581 pos = 0; 1582 ntext.clear(); 1583 while (iter.hasNext()) { 1584 match = iter.next(); 1585 ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos); 1586 const QString tagname = match.captured(1).toLower(); 1587 auto tagIt = setup.d->knownTags.constFind(tagname); 1588 if (tagIt != setup.d->knownTags.constEnd()) { 1589 const KuitTag &tag = *tagIt; 1590 ntext += tag.format(languageAsList, QHash<QString, QString>(), QString(), QStringList(), format); 1591 } else { 1592 ntext += match.captured(0); 1593 } 1594 pos = match.capturedEnd(0); 1595 } 1596 // get the remaining part after the last match in "text" 1597 ntext += QStringView(text).mid(pos); 1598 text = ntext; 1599 1600 // Add top tag. 1601 if (format == Kuit::RichText) { 1602 text = QStringLiteral("<html>") + text + QStringLiteral("</html>"); 1603 } 1604 1605 return text; 1606 } 1607 1608 KuitFormatter::KuitFormatter(const QString &language) 1609 : d(new KuitFormatterPrivate(language)) 1610 { 1611 } 1612 1613 KuitFormatter::~KuitFormatter() 1614 { 1615 delete d; 1616 } 1617 1618 QString KuitFormatter::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const 1619 { 1620 return d->format(domain, context, text, format); 1621 }