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("&amp;");
0033         } else if (c == QL1C('<')) {
0034             ntext += QStringLiteral("&lt;");
0035         } else if (c == QL1C('>')) {
0036             ntext += QStringLiteral("&gt;");
0037         } else if (c == QL1C('\'')) {
0038             ntext += QStringLiteral("&apos;");
0039         } else if (c == QL1C('"')) {
0040             ntext += QStringLiteral("&quot;");
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                        "&lt;%1&gt;"),
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                        "&lt;<i>%1</i>&gt;"),
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                        "&lt;%1&gt;"),
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                        "&lt;<a href=\"mailto:%1\">%1</a>&gt;"),
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 &lt;%2&gt;"),
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 "&amp;", 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 }