File indexing completed on 2024-04-28 15:25:22
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 KuitSetup &Kuit::setupForDomain(const char *domain) 0521 { 0522 return setupForDomain(QByteArray(domain)); 0523 } 0524 0525 class KuitSetupPrivate 0526 { 0527 public: 0528 void setTagPattern(const QString &tagName, 0529 const QStringList &attribNames, 0530 Kuit::VisualFormat format, 0531 const KLocalizedString &pattern, 0532 Kuit::TagFormatter formatter, 0533 int leadingNewlines); 0534 0535 void setTagClass(const QString &tagName, Kuit::TagClass aClass); 0536 0537 void setFormatForMarker(const QString &marker, Kuit::VisualFormat format); 0538 0539 void setDefaultMarkup(); 0540 void setDefaultFormats(); 0541 0542 QByteArray domain; 0543 QHash<QString, KuitTag> knownTags; 0544 QHash<Kuit::Role, QHash<Kuit::Cue, Kuit::VisualFormat>> formatsByRoleCue; 0545 }; 0546 0547 void KuitSetupPrivate::setTagPattern(const QString &tagName, 0548 const QStringList &attribNames_, 0549 Kuit::VisualFormat format, 0550 const KLocalizedString &pattern, 0551 Kuit::TagFormatter formatter, 0552 int leadingNewlines_) 0553 { 0554 auto tagIt = knownTags.find(tagName); 0555 if (tagIt == knownTags.end()) { 0556 tagIt = knownTags.insert(tagName, KuitTag(tagName, Kuit::PhraseTag)); 0557 } 0558 0559 KuitTag &tag = *tagIt; 0560 0561 QStringList attribNames = attribNames_; 0562 attribNames.removeAll(QString()); 0563 for (const QString &attribName : std::as_const(attribNames)) { 0564 tag.knownAttribs.insert(attribName); 0565 } 0566 QString attribKey = attributeSetKey(attribNames); 0567 tag.attributeOrders[attribKey][format] = attribNames; 0568 tag.patterns[attribKey][format] = pattern; 0569 tag.formatters[attribKey][format] = formatter; 0570 tag.leadingNewlines = leadingNewlines_; 0571 } 0572 0573 void KuitSetupPrivate::setTagClass(const QString &tagName, Kuit::TagClass aClass) 0574 { 0575 auto tagIt = knownTags.find(tagName); 0576 if (tagIt == knownTags.end()) { 0577 knownTags.insert(tagName, KuitTag(tagName, aClass)); 0578 } else { 0579 tagIt->type = aClass; 0580 } 0581 } 0582 0583 void KuitSetupPrivate::setFormatForMarker(const QString &marker, Kuit::VisualFormat format) 0584 { 0585 KuitStaticData *s = staticData(); 0586 0587 QString roleName; 0588 QString cueName; 0589 QString formatName; 0590 parseUiMarker(marker, roleName, cueName, formatName); 0591 0592 Kuit::Role role; 0593 auto roleIt = s->rolesByName.constFind(roleName); 0594 if (roleIt != s->rolesByName.constEnd()) { 0595 role = *roleIt; 0596 } else if (!roleName.isEmpty()) { 0597 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker {%2}, visual format not set.").arg(roleName, marker); 0598 return; 0599 } else { 0600 qCWarning(KI18N_KUIT) << QStringLiteral("Empty role in UI marker {%1}, visual format not set.").arg(marker); 0601 return; 0602 } 0603 0604 Kuit::Cue cue; 0605 auto cueIt = s->cuesByName.constFind(cueName); 0606 if (cueIt != s->cuesByName.constEnd()) { 0607 cue = *cueIt; 0608 if (!s->knownRoleCues.value(role).contains(cue)) { 0609 qCWarning(KI18N_KUIT) 0610 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker {%3}, visual format not set.").arg(cueName, roleName, marker); 0611 return; 0612 } 0613 } else if (!cueName.isEmpty()) { 0614 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker {%2}, visual format not set.").arg(cueName, marker); 0615 return; 0616 } else { 0617 cue = Kuit::UndefinedCue; 0618 } 0619 0620 formatsByRoleCue[role][cue] = format; 0621 } 0622 0623 #define TAG_FORMATTER_ARGS \ 0624 const QStringList &languages, const QString &tagName, const QHash<QString, QString> &attributes, const QString &text, const QStringList &tagPath, \ 0625 Kuit::VisualFormat format 0626 0627 static QString tagFormatterFilename(TAG_FORMATTER_ARGS) 0628 { 0629 Q_UNUSED(languages); 0630 Q_UNUSED(tagName); 0631 Q_UNUSED(attributes); 0632 Q_UNUSED(tagPath); 0633 #ifdef Q_OS_WIN 0634 // with rich text the path can include <foo>...</foo> which will be replaced by <foo>...<\foo> on Windows! 0635 // the same problem also happens for tags such as <br/> -> <br\> 0636 if (format == Kuit::RichText) { 0637 // replace all occurrences of "</" or "/>" to make sure toNativeSeparators() doesn't destroy XML markup 0638 const auto KUIT_CLOSE_XML_REPLACEMENT = QStringLiteral("__kuit_close_xml_tag__"); 0639 const auto KUIT_NOTEXT_XML_REPLACEMENT = QStringLiteral("__kuit_notext_xml_tag__"); 0640 0641 QString result = text; 0642 result.replace(QStringLiteral("</"), KUIT_CLOSE_XML_REPLACEMENT); 0643 result.replace(QStringLiteral("/>"), KUIT_NOTEXT_XML_REPLACEMENT); 0644 result = QDir::toNativeSeparators(result); 0645 result.replace(KUIT_CLOSE_XML_REPLACEMENT, QStringLiteral("</")); 0646 result.replace(KUIT_NOTEXT_XML_REPLACEMENT, QStringLiteral("/>")); 0647 return result; 0648 } 0649 #else 0650 Q_UNUSED(format); 0651 #endif 0652 return QDir::toNativeSeparators(text); 0653 } 0654 0655 static QString tagFormatterShortcut(TAG_FORMATTER_ARGS) 0656 { 0657 Q_UNUSED(tagName); 0658 Q_UNUSED(attributes); 0659 Q_UNUSED(tagPath); 0660 KuitStaticData *s = staticData(); 0661 return s->toKeyCombo(languages, text, format); 0662 } 0663 0664 static QString tagFormatterInterface(TAG_FORMATTER_ARGS) 0665 { 0666 Q_UNUSED(tagName); 0667 Q_UNUSED(attributes); 0668 Q_UNUSED(tagPath); 0669 KuitStaticData *s = staticData(); 0670 return s->toInterfacePath(languages, text, format); 0671 } 0672 0673 void KuitSetupPrivate::setDefaultMarkup() 0674 { 0675 using namespace Kuit; 0676 0677 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__"); 0678 const QString TITLE = QStringLiteral("title"); 0679 const QString EMPHASIS = QStringLiteral("emphasis"); 0680 const QString COMMAND = QStringLiteral("command"); 0681 const QString WARNING = QStringLiteral("warning"); 0682 const QString LINK = QStringLiteral("link"); 0683 const QString NOTE = QStringLiteral("note"); 0684 0685 // clang-format off 0686 // Macro to hide message from extraction. 0687 #define HI18NC ki18nc 0688 0689 // Macro to expedite setting the patterns. 0690 #undef SET_PATTERN 0691 #define SET_PATTERN(tagName, attribNames_, format, pattern, formatter, leadNl) \ 0692 do { \ 0693 QStringList attribNames; \ 0694 attribNames << attribNames_; \ 0695 setTagPattern(tagName, attribNames, format, pattern, formatter, leadNl); \ 0696 /* Make TermText pattern same as PlainText if not explicitly given. */ \ 0697 KuitTag &tag = knownTags[tagName]; \ 0698 QString attribKey = attributeSetKey(attribNames); \ 0699 if (format == PlainText && !tag.patterns[attribKey].contains(TermText)) { \ 0700 setTagPattern(tagName, attribNames, TermText, pattern, formatter, leadNl); \ 0701 } \ 0702 } while (0) 0703 0704 // NOTE: The following "i18n:" comments are oddly placed in order that 0705 // xgettext extracts them properly. 0706 0707 // -------> Internal top tag 0708 setTagClass(INTERNAL_TOP_TAG_NAME, StructTag); 0709 SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), PlainText, 0710 HI18NC("tag-format-pattern <> plain", 0711 // i18n: KUIT pattern, see the comment to the first of these entries above. 0712 "%1"), 0713 nullptr, 0); 0714 SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), RichText, 0715 HI18NC("tag-format-pattern <> rich", 0716 // i18n: KUIT pattern, see the comment to the first of these entries above. 0717 "%1"), 0718 nullptr, 0); 0719 0720 // -------> Title 0721 setTagClass(TITLE, StructTag); 0722 SET_PATTERN(TITLE, QString(), PlainText, 0723 ki18nc("tag-format-pattern <title> plain", 0724 // i18n: The messages with context "tag-format-pattern <tag ...> format" 0725 // are KUIT patterns for formatting the text found inside KUIT tags. 0726 // The format is either "plain" or "rich", and tells if the pattern 0727 // is used for plain text or rich text (which can use HTML tags). 0728 // You may be in general satisfied with the patterns as they are in the 0729 // original. Some things you may consider changing: 0730 // - the proper quotes, those used in msgid are English-standard 0731 // - the <i> and <b> tags, does your language script work well with them? 0732 "== %1 =="), 0733 nullptr, 2); 0734 SET_PATTERN(TITLE, QString(), RichText, 0735 ki18nc("tag-format-pattern <title> rich", 0736 // i18n: KUIT pattern, see the comment to the first of these entries above. 0737 "<h2>%1</h2>"), 0738 nullptr, 2); 0739 0740 // -------> Subtitle 0741 setTagClass(QSL("subtitle"), StructTag); 0742 SET_PATTERN(QSL("subtitle"), QString(), PlainText, 0743 ki18nc("tag-format-pattern <subtitle> plain", 0744 // i18n: KUIT pattern, see the comment to the first of these entries above. 0745 "~ %1 ~"), 0746 nullptr, 2); 0747 SET_PATTERN(QSL("subtitle"), QString(), RichText, 0748 ki18nc("tag-format-pattern <subtitle> rich", 0749 // i18n: KUIT pattern, see the comment to the first of these entries above. 0750 "<h3>%1</h3>"), 0751 nullptr, 2); 0752 0753 // -------> Para 0754 setTagClass(QSL("para"), StructTag); 0755 SET_PATTERN(QSL("para"), QString(), PlainText, 0756 ki18nc("tag-format-pattern <para> plain", 0757 // i18n: KUIT pattern, see the comment to the first of these entries above. 0758 "%1"), 0759 nullptr, 2); 0760 SET_PATTERN(QSL("para"), QString(), RichText, 0761 ki18nc("tag-format-pattern <para> rich", 0762 // i18n: KUIT pattern, see the comment to the first of these entries above. 0763 "<p>%1</p>"), 0764 nullptr, 2); 0765 0766 // -------> List 0767 setTagClass(QSL("list"), StructTag); 0768 SET_PATTERN(QSL("list"), QString(), PlainText, 0769 ki18nc("tag-format-pattern <list> plain", 0770 // i18n: KUIT pattern, see the comment to the first of these entries above. 0771 "%1"), 0772 nullptr, 1); 0773 SET_PATTERN(QSL("list"), QString(), RichText, 0774 ki18nc("tag-format-pattern <list> rich", 0775 // i18n: KUIT pattern, see the comment to the first of these entries above. 0776 "<ul>%1</ul>"), 0777 nullptr, 1); 0778 0779 // -------> Item 0780 setTagClass(QSL("item"), StructTag); 0781 SET_PATTERN(QSL("item"), QString(), PlainText, 0782 ki18nc("tag-format-pattern <item> plain", 0783 // i18n: KUIT pattern, see the comment to the first of these entries above. 0784 " * %1"), 0785 nullptr, 1); 0786 SET_PATTERN(QSL("item"), QString(), RichText, 0787 ki18nc("tag-format-pattern <item> rich", 0788 // i18n: KUIT pattern, see the comment to the first of these entries above. 0789 "<li>%1</li>"), 0790 nullptr, 1); 0791 0792 // -------> Note 0793 SET_PATTERN(NOTE, QString(), PlainText, 0794 ki18nc("tag-format-pattern <note> plain", 0795 // i18n: KUIT pattern, see the comment to the first of these entries above. 0796 "Note: %1"), 0797 nullptr, 0); 0798 SET_PATTERN(NOTE, QString(), RichText, 0799 ki18nc("tag-format-pattern <note> rich", 0800 // i18n: KUIT pattern, see the comment to the first of these entries above. 0801 "<i>Note</i>: %1"), 0802 nullptr, 0); 0803 SET_PATTERN(NOTE, QSL("label"), PlainText, 0804 ki18nc("tag-format-pattern <note label=> plain\n" 0805 "%1 is the text, %2 is the note label", 0806 // i18n: KUIT pattern, see the comment to the first of these entries above. 0807 "%2: %1"), 0808 nullptr, 0); 0809 SET_PATTERN(NOTE, QSL("label"), RichText, 0810 ki18nc("tag-format-pattern <note label=> rich\n" 0811 "%1 is the text, %2 is the note label", 0812 // i18n: KUIT pattern, see the comment to the first of these entries above. 0813 "<i>%2</i>: %1"), 0814 nullptr, 0); 0815 0816 // -------> Warning 0817 SET_PATTERN(WARNING, QString(), PlainText, 0818 ki18nc("tag-format-pattern <warning> plain", 0819 // i18n: KUIT pattern, see the comment to the first of these entries above. 0820 "WARNING: %1"), 0821 nullptr, 0); 0822 SET_PATTERN(WARNING, QString(), RichText, 0823 ki18nc("tag-format-pattern <warning> rich", 0824 // i18n: KUIT pattern, see the comment to the first of these entries above. 0825 "<b>Warning</b>: %1"), 0826 nullptr, 0); 0827 SET_PATTERN(WARNING, QSL("label"), PlainText, 0828 ki18nc("tag-format-pattern <warning label=> plain\n" 0829 "%1 is the text, %2 is the warning label", 0830 // i18n: KUIT pattern, see the comment to the first of these entries above. 0831 "%2: %1"), 0832 nullptr, 0); 0833 SET_PATTERN(WARNING, QSL("label"), RichText, 0834 ki18nc("tag-format-pattern <warning label=> rich\n" 0835 "%1 is the text, %2 is the warning label", 0836 // i18n: KUIT pattern, see the comment to the first of these entries above. 0837 "<b>%2</b>: %1"), 0838 nullptr, 0); 0839 0840 // -------> Link 0841 SET_PATTERN(LINK, QString(), PlainText, 0842 ki18nc("tag-format-pattern <link> plain", 0843 // i18n: KUIT pattern, see the comment to the first of these entries above. 0844 "%1"), 0845 nullptr, 0); 0846 SET_PATTERN(LINK, QString(), RichText, 0847 ki18nc("tag-format-pattern <link> rich", 0848 // i18n: KUIT pattern, see the comment to the first of these entries above. 0849 "<a href=\"%1\">%1</a>"), 0850 nullptr, 0); 0851 SET_PATTERN(LINK, QSL("url"), PlainText, 0852 ki18nc("tag-format-pattern <link url=> plain\n" 0853 "%1 is the descriptive text, %2 is the URL", 0854 // i18n: KUIT pattern, see the comment to the first of these entries above. 0855 "%1 (%2)"), 0856 nullptr, 0); 0857 SET_PATTERN(LINK, QSL("url"), RichText, 0858 ki18nc("tag-format-pattern <link url=> rich\n" 0859 "%1 is the descriptive text, %2 is the URL", 0860 // i18n: KUIT pattern, see the comment to the first of these entries above. 0861 "<a href=\"%2\">%1</a>"), 0862 nullptr, 0); 0863 0864 // -------> Filename 0865 SET_PATTERN(QSL("filename"), QString(), PlainText, 0866 ki18nc("tag-format-pattern <filename> plain", 0867 // i18n: KUIT pattern, see the comment to the first of these entries above. 0868 "‘%1’"), 0869 tagFormatterFilename, 0); 0870 SET_PATTERN(QSL("filename"), QString(), RichText, 0871 ki18nc("tag-format-pattern <filename> rich", 0872 // i18n: KUIT pattern, see the comment to the first of these entries above. 0873 "‘<tt>%1</tt>’"), 0874 tagFormatterFilename, 0); 0875 0876 // -------> Application 0877 SET_PATTERN(QSL("application"), QString(), PlainText, 0878 ki18nc("tag-format-pattern <application> plain", 0879 // i18n: KUIT pattern, see the comment to the first of these entries above. 0880 "%1"), 0881 nullptr, 0); 0882 SET_PATTERN(QSL("application"), QString(), RichText, 0883 ki18nc("tag-format-pattern <application> rich", 0884 // i18n: KUIT pattern, see the comment to the first of these entries above. 0885 "%1"), 0886 nullptr, 0); 0887 0888 // -------> Command 0889 SET_PATTERN(COMMAND, QString(), PlainText, 0890 ki18nc("tag-format-pattern <command> plain", 0891 // i18n: KUIT pattern, see the comment to the first of these entries above. 0892 "%1"), 0893 nullptr, 0); 0894 SET_PATTERN(COMMAND, QString(), RichText, 0895 ki18nc("tag-format-pattern <command> rich", 0896 // i18n: KUIT pattern, see the comment to the first of these entries above. 0897 "<tt>%1</tt>"), 0898 nullptr, 0); 0899 SET_PATTERN(COMMAND, QSL("section"), PlainText, 0900 ki18nc("tag-format-pattern <command section=> plain\n" 0901 "%1 is the command name, %2 is its man section", 0902 // i18n: KUIT pattern, see the comment to the first of these entries above. 0903 "%1(%2)"), 0904 nullptr, 0); 0905 SET_PATTERN(COMMAND, QSL("section"), RichText, 0906 ki18nc("tag-format-pattern <command section=> rich\n" 0907 "%1 is the command name, %2 is its man section", 0908 // i18n: KUIT pattern, see the comment to the first of these entries above. 0909 "<tt>%1(%2)</tt>"), 0910 nullptr, 0); 0911 0912 // -------> Resource 0913 SET_PATTERN(QSL("resource"), QString(), PlainText, 0914 ki18nc("tag-format-pattern <resource> plain", 0915 // i18n: KUIT pattern, see the comment to the first of these entries above. 0916 "“%1”"), 0917 nullptr, 0); 0918 SET_PATTERN(QSL("resource"), QString(), RichText, 0919 ki18nc("tag-format-pattern <resource> rich", 0920 // i18n: KUIT pattern, see the comment to the first of these entries above. 0921 "“%1”"), 0922 nullptr, 0); 0923 0924 // -------> Icode 0925 SET_PATTERN(QSL("icode"), QString(), PlainText, 0926 ki18nc("tag-format-pattern <icode> plain", 0927 // i18n: KUIT pattern, see the comment to the first of these entries above. 0928 "“%1”"), 0929 nullptr, 0); 0930 SET_PATTERN(QSL("icode"), QString(), RichText, 0931 ki18nc("tag-format-pattern <icode> rich", 0932 // i18n: KUIT pattern, see the comment to the first of these entries above. 0933 "<tt>%1</tt>"), 0934 nullptr, 0); 0935 0936 // -------> Bcode 0937 SET_PATTERN(QSL("bcode"), QString(), PlainText, 0938 ki18nc("tag-format-pattern <bcode> plain", 0939 // i18n: KUIT pattern, see the comment to the first of these entries above. 0940 "\n%1\n"), 0941 nullptr, 2); 0942 SET_PATTERN(QSL("bcode"), QString(), RichText, 0943 ki18nc("tag-format-pattern <bcode> rich", 0944 // i18n: KUIT pattern, see the comment to the first of these entries above. 0945 "<pre>%1</pre>"), 0946 nullptr, 2); 0947 0948 // -------> Shortcut 0949 SET_PATTERN(QSL("shortcut"), QString(), PlainText, 0950 ki18nc("tag-format-pattern <shortcut> plain", 0951 // i18n: KUIT pattern, see the comment to the first of these entries above. 0952 "%1"), 0953 tagFormatterShortcut, 0); 0954 SET_PATTERN(QSL("shortcut"), QString(), RichText, 0955 ki18nc("tag-format-pattern <shortcut> rich", 0956 // i18n: KUIT pattern, see the comment to the first of these entries above. 0957 "<b>%1</b>"), 0958 tagFormatterShortcut, 0); 0959 0960 // -------> Interface 0961 SET_PATTERN(QSL("interface"), QString(), PlainText, 0962 ki18nc("tag-format-pattern <interface> plain", 0963 // i18n: KUIT pattern, see the comment to the first of these entries above. 0964 "|%1|"), 0965 tagFormatterInterface, 0); 0966 SET_PATTERN(QSL("interface"), QString(), RichText, 0967 ki18nc("tag-format-pattern <interface> rich", 0968 // i18n: KUIT pattern, see the comment to the first of these entries above. 0969 "<i>%1</i>"), 0970 tagFormatterInterface, 0); 0971 0972 // -------> Emphasis 0973 SET_PATTERN(EMPHASIS, QString(), PlainText, 0974 ki18nc("tag-format-pattern <emphasis> plain", 0975 // i18n: KUIT pattern, see the comment to the first of these entries above. 0976 "*%1*"), 0977 nullptr, 0); 0978 SET_PATTERN(EMPHASIS, QString(), RichText, 0979 ki18nc("tag-format-pattern <emphasis> rich", 0980 // i18n: KUIT pattern, see the comment to the first of these entries above. 0981 "<i>%1</i>"), 0982 nullptr, 0); 0983 SET_PATTERN(EMPHASIS, QSL("strong"), PlainText, 0984 ki18nc("tag-format-pattern <emphasis-strong> plain", 0985 // i18n: KUIT pattern, see the comment to the first of these entries above. 0986 "**%1**"), 0987 nullptr, 0); 0988 SET_PATTERN(EMPHASIS, QSL("strong"), RichText, 0989 ki18nc("tag-format-pattern <emphasis-strong> rich", 0990 // i18n: KUIT pattern, see the comment to the first of these entries above. 0991 "<b>%1</b>"), 0992 nullptr, 0); 0993 0994 // -------> Placeholder 0995 SET_PATTERN(QSL("placeholder"), QString(), PlainText, 0996 ki18nc("tag-format-pattern <placeholder> plain", 0997 // i18n: KUIT pattern, see the comment to the first of these entries above. 0998 "<%1>"), 0999 nullptr, 0); 1000 SET_PATTERN(QSL("placeholder"), QString(), RichText, 1001 ki18nc("tag-format-pattern <placeholder> rich", 1002 // i18n: KUIT pattern, see the comment to the first of these entries above. 1003 "<<i>%1</i>>"), 1004 nullptr, 0); 1005 1006 // -------> Email 1007 SET_PATTERN(QSL("email"), QString(), PlainText, 1008 ki18nc("tag-format-pattern <email> plain", 1009 // i18n: KUIT pattern, see the comment to the first of these entries above. 1010 "<%1>"), 1011 nullptr, 0); 1012 SET_PATTERN(QSL("email"), QString(), RichText, 1013 ki18nc("tag-format-pattern <email> rich", 1014 // i18n: KUIT pattern, see the comment to the first of these entries above. 1015 "<<a href=\"mailto:%1\">%1</a>>"), 1016 nullptr, 0); 1017 SET_PATTERN(QSL("email"), QSL("address"), PlainText, 1018 ki18nc("tag-format-pattern <email address=> plain\n" 1019 "%1 is name, %2 is address", 1020 // i18n: KUIT pattern, see the comment to the first of these entries above. 1021 "%1 <%2>"), 1022 nullptr, 0); 1023 SET_PATTERN(QSL("email"), QSL("address"), RichText, 1024 ki18nc("tag-format-pattern <email address=> rich\n" 1025 "%1 is name, %2 is address", 1026 // i18n: KUIT pattern, see the comment to the first of these entries above. 1027 "<a href=\"mailto:%2\">%1</a>"), 1028 nullptr, 0); 1029 1030 // -------> Envar 1031 SET_PATTERN(QSL("envar"), QString(), PlainText, 1032 ki18nc("tag-format-pattern <envar> plain", 1033 // i18n: KUIT pattern, see the comment to the first of these entries above. 1034 "$%1"), 1035 nullptr, 0); 1036 SET_PATTERN(QSL("envar"), QString(), RichText, 1037 ki18nc("tag-format-pattern <envar> rich", 1038 // i18n: KUIT pattern, see the comment to the first of these entries above. 1039 "<tt>$%1</tt>"), 1040 nullptr, 0); 1041 1042 // -------> Message 1043 SET_PATTERN(QSL("message"), QString(), PlainText, 1044 ki18nc("tag-format-pattern <message> plain", 1045 // i18n: KUIT pattern, see the comment to the first of these entries above. 1046 "/%1/"), 1047 nullptr, 0); 1048 SET_PATTERN(QSL("message"), QString(), RichText, 1049 ki18nc("tag-format-pattern <message> rich", 1050 // i18n: KUIT pattern, see the comment to the first of these entries above. 1051 "<i>%1</i>"), 1052 nullptr, 0); 1053 1054 // -------> Nl 1055 SET_PATTERN(QSL("nl"), QString(), PlainText, 1056 ki18nc("tag-format-pattern <nl> plain", 1057 // i18n: KUIT pattern, see the comment to the first of these entries above. 1058 "%1\n"), 1059 nullptr, 0); 1060 SET_PATTERN(QSL("nl"), QString(), RichText, 1061 ki18nc("tag-format-pattern <nl> rich", 1062 // i18n: KUIT pattern, see the comment to the first of these entries above. 1063 "%1<br/>"), 1064 nullptr, 0); 1065 // clang-format on 1066 } 1067 1068 void KuitSetupPrivate::setDefaultFormats() 1069 { 1070 using namespace Kuit; 1071 1072 // Setup formats by role. 1073 formatsByRoleCue[ActionRole][UndefinedCue] = PlainText; 1074 formatsByRoleCue[TitleRole][UndefinedCue] = PlainText; 1075 formatsByRoleCue[LabelRole][UndefinedCue] = PlainText; 1076 formatsByRoleCue[OptionRole][UndefinedCue] = PlainText; 1077 formatsByRoleCue[ItemRole][UndefinedCue] = PlainText; 1078 formatsByRoleCue[InfoRole][UndefinedCue] = RichText; 1079 1080 // Setup override formats by subcue. 1081 formatsByRoleCue[InfoRole][StatusCue] = PlainText; 1082 formatsByRoleCue[InfoRole][ProgressCue] = PlainText; 1083 formatsByRoleCue[InfoRole][CreditCue] = PlainText; 1084 formatsByRoleCue[InfoRole][ShellCue] = TermText; 1085 } 1086 1087 KuitSetup::KuitSetup(const QByteArray &domain) 1088 : d(new KuitSetupPrivate) 1089 { 1090 d->domain = domain; 1091 d->setDefaultMarkup(); 1092 d->setDefaultFormats(); 1093 } 1094 1095 KuitSetup::~KuitSetup() = default; 1096 1097 void KuitSetup::setTagPattern(const QString &tagName, 1098 const QStringList &attribNames, 1099 Kuit::VisualFormat format, 1100 const KLocalizedString &pattern, 1101 Kuit::TagFormatter formatter, 1102 int leadingNewlines) 1103 { 1104 d->setTagPattern(tagName, attribNames, format, pattern, formatter, leadingNewlines); 1105 } 1106 1107 void KuitSetup::setTagClass(const QString &tagName, Kuit::TagClass aClass) 1108 { 1109 d->setTagClass(tagName, aClass); 1110 } 1111 1112 void KuitSetup::setFormatForMarker(const QString &marker, Kuit::VisualFormat format) 1113 { 1114 d->setFormatForMarker(marker, format); 1115 } 1116 1117 class KuitFormatterPrivate 1118 { 1119 public: 1120 KuitFormatterPrivate(const QString &language); 1121 1122 QString format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const; 1123 1124 // Get metatranslation (formatting patterns, etc.) 1125 QString metaTr(const char *context, const char *text) const; 1126 1127 // Set visual formatting patterns for text within tags. 1128 void setFormattingPatterns(); 1129 1130 // Set data used in transformation of text within tags. 1131 void setTextTransformData(); 1132 1133 // Determine visual format by parsing the UI marker in the context. 1134 static Kuit::VisualFormat formatFromUiMarker(const QString &context, const KuitSetup &setup); 1135 1136 // Determine if text has block structure (multiple paragraphs, etc). 1137 static bool determineIsStructured(const QString &text, const KuitSetup &setup); 1138 1139 // Format KUIT text into visual text. 1140 QString toVisualText(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const; 1141 1142 // Final touches to the formatted text. 1143 QString finalizeVisualText(const QString &ftext, Kuit::VisualFormat format) const; 1144 1145 // In case of markup errors, try to make result not look too bad. 1146 QString salvageMarkup(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const; 1147 1148 // Data for XML parsing state. 1149 class OpenEl 1150 { 1151 public: 1152 enum Handling { Proper, Ignored, Dropout }; 1153 1154 QString name; 1155 QHash<QString, QString> attributes; 1156 QString attribStr; 1157 Handling handling; 1158 QString formattedText; 1159 QStringList tagPath; 1160 }; 1161 1162 // Gather data about current element for the parse state. 1163 KuitFormatterPrivate::OpenEl parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const; 1164 1165 // Format text of the element. 1166 QString formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const; 1167 1168 // Count number of newlines at start and at end of text. 1169 static void countWrappingNewlines(const QString &ptext, int &numle, int &numtr); 1170 1171 private: 1172 QString language; 1173 QStringList languageAsList; 1174 1175 QHash<Kuit::VisualFormat, QString> comboKeyDelim; 1176 QHash<Kuit::VisualFormat, QString> guiPathDelim; 1177 1178 QHash<QString, QString> keyNames; 1179 }; 1180 1181 KuitFormatterPrivate::KuitFormatterPrivate(const QString &language_) 1182 : language(language_) 1183 { 1184 } 1185 1186 QString KuitFormatterPrivate::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const 1187 { 1188 const KuitSetup &setup = Kuit::setupForDomain(domain); 1189 1190 // If format is undefined, determine it based on UI marker inside context. 1191 Kuit::VisualFormat resolvedFormat = format; 1192 if (resolvedFormat == Kuit::UndefinedFormat) { 1193 resolvedFormat = formatFromUiMarker(context, setup); 1194 } 1195 1196 // Quick check: are there any tags at all? 1197 QString ftext; 1198 if (text.indexOf(QL1C('<')) < 0) { 1199 ftext = finalizeVisualText(text, resolvedFormat); 1200 } else { 1201 // Format the text. 1202 ftext = toVisualText(text, resolvedFormat, setup); 1203 if (ftext.isEmpty()) { // error while processing markup 1204 ftext = salvageMarkup(text, resolvedFormat, setup); 1205 } 1206 } 1207 return ftext; 1208 } 1209 1210 Kuit::VisualFormat KuitFormatterPrivate::formatFromUiMarker(const QString &context, const KuitSetup &setup) 1211 { 1212 KuitStaticData *s = staticData(); 1213 1214 QString roleName; 1215 QString cueName; 1216 QString formatName; 1217 parseUiMarker(context, roleName, cueName, formatName); 1218 1219 // Set role from name. 1220 Kuit::Role role = s->rolesByName.value(roleName, Kuit::UndefinedRole); 1221 if (role == Kuit::UndefinedRole) { // unknown role 1222 if (!roleName.isEmpty()) { 1223 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker in context {%2}.").arg(roleName, shorten(context)); 1224 } 1225 } 1226 1227 // Set subcue from name. 1228 Kuit::Cue cue; 1229 if (role != Kuit::UndefinedRole) { 1230 cue = s->cuesByName.value(cueName, Kuit::UndefinedCue); 1231 if (cue != Kuit::UndefinedCue) { // known subcue 1232 if (!s->knownRoleCues.value(role).contains(cue)) { 1233 cue = Kuit::UndefinedCue; 1234 qCWarning(KI18N_KUIT) 1235 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker in context {%3}.").arg(cueName, roleName, shorten(context)); 1236 } 1237 } else { // unknown or not given subcue 1238 if (!cueName.isEmpty()) { 1239 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker in context {%2}.").arg(cueName, shorten(context)); 1240 } 1241 } 1242 } else { 1243 // Bad role, silently ignore the cue. 1244 cue = Kuit::UndefinedCue; 1245 } 1246 1247 // Set format from name, or by derivation from context/subcue. 1248 Kuit::VisualFormat format = s->formatsByName.value(formatName, Kuit::UndefinedFormat); 1249 if (format == Kuit::UndefinedFormat) { // unknown or not given format 1250 // Check first if there is a format defined for role/subcue 1251 // combination, then for role only, otherwise default to undefined. 1252 auto formatsByCueIt = setup.d->formatsByRoleCue.constFind(role); 1253 if (formatsByCueIt != setup.d->formatsByRoleCue.constEnd()) { 1254 const auto &formatsByCue = *formatsByCueIt; 1255 auto formatIt = formatsByCue.constFind(cue); 1256 if (formatIt != formatsByCue.constEnd()) { 1257 format = *formatIt; 1258 } else { 1259 format = formatsByCue.value(Kuit::UndefinedCue); 1260 } 1261 } 1262 if (!formatName.isEmpty()) { 1263 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown format '/%1' in UI marker for message {%2}.").arg(formatName, shorten(context)); 1264 } 1265 } 1266 if (format == Kuit::UndefinedFormat) { 1267 format = Kuit::PlainText; 1268 } 1269 1270 return format; 1271 } 1272 1273 bool KuitFormatterPrivate::determineIsStructured(const QString &text, const KuitSetup &setup) 1274 { 1275 // If the text opens with a structuring tag, then it is structured, 1276 // otherwise not. Leading whitespace is ignored for this purpose. 1277 static const QRegularExpression opensWithTagRx(QStringLiteral("^\\s*<\\s*(\\w+)[^>]*>")); 1278 bool isStructured = false; 1279 const QRegularExpressionMatch match = opensWithTagRx.match(text); 1280 if (match.hasMatch()) { 1281 const QString tagName = match.captured(1).toLower(); 1282 auto tagIt = setup.d->knownTags.constFind(tagName); 1283 if (tagIt != setup.d->knownTags.constEnd()) { 1284 const KuitTag &tag = *tagIt; 1285 isStructured = (tag.type == Kuit::StructTag); 1286 } 1287 } 1288 return isStructured; 1289 } 1290 1291 static const char s_entitySubRx[] = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+"; 1292 1293 QString KuitFormatterPrivate::toVisualText(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const 1294 { 1295 KuitStaticData *s = staticData(); 1296 1297 // Replace &-shortcut marker with "&", not to confuse the parser; 1298 // but do not touch & which forms an XML entity as it is. 1299 QString original = text_; 1300 // Regex is (see s_entitySubRx var): ^([a-z]+|#[0-9]+|#x[0-9a-fA-F]+); 1301 static const QRegularExpression restRx(QLatin1String("^(") + QLatin1String(s_entitySubRx) + QLatin1String(");")); 1302 1303 QString text; 1304 int p = original.indexOf(QL1C('&')); 1305 while (p >= 0) { 1306 text.append(QStringView(original).mid(0, p + 1)); 1307 original.remove(0, p + 1); 1308 if (original.indexOf(restRx) != 0) { // not an entity 1309 text.append(QSL("amp;")); 1310 } 1311 p = original.indexOf(QL1C('&')); 1312 } 1313 text.append(original); 1314 1315 // FIXME: Do this and then check proper use of structuring and phrase tags. 1316 #if 0 1317 // Determine whether this is block-structured text. 1318 bool isStructured = determineIsStructured(text, setup); 1319 #endif 1320 1321 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__"); 1322 // Add top tag, not to confuse the parser. 1323 text = QStringLiteral("<%2>%1</%2>").arg(text, INTERNAL_TOP_TAG_NAME); 1324 1325 QStack<OpenEl> openEls; 1326 QXmlStreamReader xml(text); 1327 xml.setEntityResolver(&s->xmlEntityResolver); 1328 QStringView lastElementName; 1329 1330 while (!xml.atEnd()) { 1331 xml.readNext(); 1332 1333 if (xml.isStartElement()) { 1334 lastElementName = xml.name(); 1335 1336 OpenEl oel; 1337 1338 if (openEls.isEmpty()) { 1339 // Must be the root element. 1340 oel.name = INTERNAL_TOP_TAG_NAME; 1341 oel.handling = OpenEl::Proper; 1342 } else { 1343 // Find first proper enclosing element. 1344 OpenEl enclosingOel; 1345 for (int i = openEls.size() - 1; i >= 0; --i) { 1346 if (openEls[i].handling == OpenEl::Proper) { 1347 enclosingOel = openEls[i]; 1348 break; 1349 } 1350 } 1351 // Collect data about this element. 1352 oel = parseOpenEl(xml, enclosingOel, text, setup); 1353 } 1354 1355 // Record the new element on the parse stack. 1356 openEls.push(oel); 1357 } else if (xml.isEndElement()) { 1358 // Get closed element data. 1359 OpenEl oel = openEls.pop(); 1360 1361 // If this was closing of the top element, we're done. 1362 if (openEls.isEmpty()) { 1363 // Return with final touches applied. 1364 return finalizeVisualText(oel.formattedText, format); 1365 } 1366 1367 // Append formatted text segment. 1368 QString ptext = openEls.top().formattedText; // preceding text 1369 openEls.top().formattedText += formatSubText(ptext, oel, format, setup); 1370 } else if (xml.isCharacters()) { 1371 // Stream reader will automatically resolve default XML entities, 1372 // which is not desired in this case, as the entities are to be 1373 // resolved in finalizeVisualText. Convert back into entities. 1374 const QString ctext = xml.text().toString(); 1375 QString nctext; 1376 for (const QChar c : ctext) { 1377 auto nameIt = s->xmlEntitiesInverse.constFind(c); 1378 if (nameIt != s->xmlEntitiesInverse.constEnd()) { 1379 const QString &entName = *nameIt; 1380 nctext += QL1C('&') + entName + QL1C(';'); 1381 } else { 1382 nctext += c; 1383 } 1384 } 1385 openEls.top().formattedText += nctext; 1386 } 1387 } 1388 1389 if (xml.hasError()) { 1390 qCWarning(KI18N_KUIT) << QStringLiteral("Markup error in message {%1}: %2. Last tag parsed: %3. Complete message follows:\n%4") 1391 .arg(shorten(text), xml.errorString(), lastElementName.toString(), text); 1392 return QString(); 1393 } 1394 1395 // Cannot reach here. 1396 return text; 1397 } 1398 1399 KuitFormatterPrivate::OpenEl 1400 KuitFormatterPrivate::parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const 1401 { 1402 OpenEl oel; 1403 oel.name = xml.name().toString().toLower(); 1404 1405 // Collect attribute names and values, and format attribute string. 1406 QStringList attribNames; 1407 QStringList attribValues; 1408 const auto listAttributes = xml.attributes(); 1409 attribNames.reserve(listAttributes.size()); 1410 attribValues.reserve(listAttributes.size()); 1411 for (const QXmlStreamAttribute &xatt : listAttributes) { 1412 attribNames += xatt.name().toString().toLower(); 1413 attribValues += xatt.value().toString(); 1414 QChar qc = attribValues.last().indexOf(QL1C('\'')) < 0 ? QL1C('\'') : QL1C('"'); 1415 oel.attribStr += QL1C(' ') + attribNames.last() + QL1C('=') + qc + attribValues.last() + qc; 1416 } 1417 1418 auto tagIt = setup.d->knownTags.constFind(oel.name); 1419 if (tagIt != setup.d->knownTags.constEnd()) { // known KUIT element 1420 const KuitTag &tag = *tagIt; 1421 const KuitTag &etag = setup.d->knownTags.value(enclosingOel.name); 1422 1423 // If this element can be contained within enclosing element, 1424 // mark it proper, otherwise mark it for removal. 1425 if (tag.name.isEmpty() || tag.type == Kuit::PhraseTag || etag.type == Kuit::StructTag) { 1426 oel.handling = OpenEl::Proper; 1427 } else { 1428 oel.handling = OpenEl::Dropout; 1429 qCWarning(KI18N_KUIT) 1430 << QStringLiteral("Structuring tag ('%1') cannot be subtag of phrase tag ('%2') in message {%3}.").arg(tag.name, etag.name, shorten(text)); 1431 } 1432 1433 // Resolve attributes and compute attribute set key. 1434 QSet<QString> attset; 1435 for (int i = 0; i < attribNames.size(); ++i) { 1436 QString att = attribNames[i]; 1437 if (tag.knownAttribs.contains(att)) { 1438 attset << att; 1439 oel.attributes[att] = attribValues[i]; 1440 } else { 1441 qCWarning(KI18N_KUIT) << QStringLiteral("Attribute '%1' not defined for tag '%2' in message {%3}.").arg(att, tag.name, shorten(text)); 1442 } 1443 } 1444 1445 // Continue tag path. 1446 oel.tagPath = enclosingOel.tagPath; 1447 oel.tagPath.prepend(enclosingOel.name); 1448 1449 } else { // unknown element, leave it in verbatim 1450 oel.handling = OpenEl::Ignored; 1451 qCWarning(KI18N_KUIT) << QStringLiteral("Tag '%1' is not defined in message {%2}.").arg(oel.name, shorten(text)); 1452 } 1453 1454 return oel; 1455 } 1456 1457 QString KuitFormatterPrivate::formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const 1458 { 1459 if (oel.handling == OpenEl::Proper) { 1460 const KuitTag &tag = setup.d->knownTags.value(oel.name); 1461 QString ftext = tag.format(languageAsList, oel.attributes, oel.formattedText, oel.tagPath, format); 1462 1463 // Handle leading newlines, if this is not start of the text 1464 // (ptext is the preceding text). 1465 if (!ptext.isEmpty() && tag.leadingNewlines > 0) { 1466 // Count number of present newlines. 1467 int pnumle; 1468 int pnumtr; 1469 int fnumle; 1470 int fnumtr; 1471 countWrappingNewlines(ptext, pnumle, pnumtr); 1472 countWrappingNewlines(ftext, fnumle, fnumtr); 1473 // Number of leading newlines already present. 1474 int numle = pnumtr + fnumle; 1475 // The required extra newlines. 1476 QString strle; 1477 if (numle < tag.leadingNewlines) { 1478 strle = QString(tag.leadingNewlines - numle, QL1C('\n')); 1479 } 1480 ftext = strle + ftext; 1481 } 1482 1483 return ftext; 1484 1485 } else if (oel.handling == OpenEl::Ignored) { 1486 return QL1C('<') + oel.name + oel.attribStr + QL1C('>') + oel.formattedText + QSL("</") + oel.name + QL1C('>'); 1487 1488 } else { // oel.handling == OpenEl::Dropout 1489 return oel.formattedText; 1490 } 1491 } 1492 1493 void KuitFormatterPrivate::countWrappingNewlines(const QString &text, int &numle, int &numtr) 1494 { 1495 int len = text.length(); 1496 // Number of newlines at start of text. 1497 numle = 0; 1498 while (numle < len && text[numle] == QL1C('\n')) { 1499 ++numle; 1500 } 1501 // Number of newlines at end of text. 1502 numtr = 0; 1503 while (numtr < len && text[len - numtr - 1] == QL1C('\n')) { 1504 ++numtr; 1505 } 1506 } 1507 1508 QString KuitFormatterPrivate::finalizeVisualText(const QString &text_, Kuit::VisualFormat format) const 1509 { 1510 KuitStaticData *s = staticData(); 1511 1512 QString text = text_; 1513 1514 // Resolve XML entities. 1515 if (format != Kuit::RichText) { 1516 // regex is (see s_entitySubRx var): (&([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);) 1517 static const QRegularExpression entRx(QLatin1String("(&(") + QLatin1String(s_entitySubRx) + QLatin1String(");)")); 1518 QRegularExpressionMatch match; 1519 QString plain; 1520 while ((match = entRx.match(text)).hasMatch()) { 1521 plain.append(QStringView(text).mid(0, match.capturedStart(0))); 1522 text.remove(0, match.capturedEnd(0)); 1523 const QString ent = match.captured(2); 1524 if (ent.startsWith(QL1C('#'))) { // numeric character entity 1525 bool ok; 1526 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 1527 QStringView entView(ent); 1528 const QChar c = ent.at(1) == QL1C('x') ? QChar(entView.mid(2).toInt(&ok, 16)) : QChar(entView.mid(1).toInt(&ok, 10)); 1529 #else 1530 const QChar c = ent.at(1) == QL1C('x') ? QChar(ent.midRef(2).toInt(&ok, 16)) : QChar(ent.midRef(1).toInt(&ok, 10)); 1531 #endif 1532 if (ok) { 1533 plain.append(c); 1534 } else { // unknown Unicode point, leave as is 1535 plain.append(match.capturedView(0)); 1536 } 1537 } else if (s->xmlEntities.contains(ent)) { // known entity 1538 plain.append(s->xmlEntities[ent]); 1539 } else { // unknown entity, just leave as is 1540 plain.append(match.capturedView(0)); 1541 } 1542 } 1543 plain.append(text); 1544 text = plain; 1545 } 1546 1547 // Add top tag. 1548 if (format == Kuit::RichText) { 1549 text = QLatin1String("<html>") + text + QLatin1String("</html>"); 1550 } 1551 1552 return text; 1553 } 1554 1555 QString KuitFormatterPrivate::salvageMarkup(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const 1556 { 1557 QString text = text_; 1558 QString ntext; 1559 1560 // Resolve tags simple-mindedly. 1561 1562 // - tags with content 1563 static const QRegularExpression wrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)"), QRegularExpression::InvertedGreedinessOption); 1564 QRegularExpressionMatchIterator iter = wrapRx.globalMatch(text); 1565 QRegularExpressionMatch match; 1566 int pos = 0; 1567 while (iter.hasNext()) { 1568 match = iter.next(); 1569 ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos); 1570 const QString tagname = match.captured(2).toLower(); 1571 const QString content = salvageMarkup(match.captured(4), format, setup); 1572 auto tagIt = setup.d->knownTags.constFind(tagname); 1573 if (tagIt != setup.d->knownTags.constEnd()) { 1574 const KuitTag &tag = *tagIt; 1575 QHash<QString, QString> attributes; 1576 // TODO: Do not ignore attributes (in match.captured(3)). 1577 ntext += tag.format(languageAsList, attributes, content, QStringList(), format); 1578 } else { 1579 ntext += match.captured(1) + content + match.captured(5); 1580 } 1581 pos = match.capturedEnd(0); 1582 } 1583 // get the remaining part after the last match in "text" 1584 ntext += QStringView(text).mid(pos); 1585 text = ntext; 1586 1587 // - tags without content 1588 static const QRegularExpression nowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>"), QRegularExpression::InvertedGreedinessOption); 1589 iter = nowrRx.globalMatch(text); 1590 pos = 0; 1591 ntext.clear(); 1592 while (iter.hasNext()) { 1593 match = iter.next(); 1594 ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos); 1595 const QString tagname = match.captured(1).toLower(); 1596 auto tagIt = setup.d->knownTags.constFind(tagname); 1597 if (tagIt != setup.d->knownTags.constEnd()) { 1598 const KuitTag &tag = *tagIt; 1599 ntext += tag.format(languageAsList, QHash<QString, QString>(), QString(), QStringList(), format); 1600 } else { 1601 ntext += match.captured(0); 1602 } 1603 pos = match.capturedEnd(0); 1604 } 1605 // get the remaining part after the last match in "text" 1606 ntext += QStringView(text).mid(pos); 1607 text = ntext; 1608 1609 // Add top tag. 1610 if (format == Kuit::RichText) { 1611 text = QStringLiteral("<html>") + text + QStringLiteral("</html>"); 1612 } 1613 1614 return text; 1615 } 1616 1617 KuitFormatter::KuitFormatter(const QString &language) 1618 : d(new KuitFormatterPrivate(language)) 1619 { 1620 } 1621 1622 KuitFormatter::~KuitFormatter() 1623 { 1624 delete d; 1625 } 1626 1627 QString KuitFormatter::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const 1628 { 1629 return d->format(domain, context, text, format); 1630 }