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("&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 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                        "&lt;%1&gt;"),
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                        "&lt;<i>%1</i>&gt;"),
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                        "&lt;%1&gt;"),
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                        "&lt;<a href=\"mailto:%1\">%1</a>&gt;"),
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 &lt;%2&gt;"),
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 "&amp;", 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 }