1 /*  This file is part of the KDE libraries
2     SPDX-FileCopyrightText: 2007, 2013 Chusslove Illich <caslav.ilic@gmx.net>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include <QDir>
8 #include <QPair>
9 #include <QRegularExpression>
10 #include <QSet>
11 #include <QStack>
12 #include <QXmlStreamReader>
13 
14 #include <klazylocalizedstring.h>
15 #include <klocalizedstring.h>
16 #include <kuitmarkup.h>
17 #include <kuitmarkup_p.h>
18 
19 #include "ki18n_logging_kuit.h"
20 
21 #define QL1S(x) QLatin1String(x)
22 #define QSL(x) QStringLiteral(x)
23 #define QL1C(x) QLatin1Char(x)
24 
escape(const QString & text)25 QString Kuit::escape(const QString &text)
26 {
27     int tlen = text.length();
28     QString ntext;
29     ntext.reserve(tlen);
30     for (int i = 0; i < tlen; ++i) {
31         QChar c = text[i];
32         if (c == QL1C('&')) {
33             ntext += QStringLiteral("&amp;");
34         } else if (c == QL1C('<')) {
35             ntext += QStringLiteral("&lt;");
36         } else if (c == QL1C('>')) {
37             ntext += QStringLiteral("&gt;");
38         } else if (c == QL1C('\'')) {
39             ntext += QStringLiteral("&apos;");
40         } else if (c == QL1C('"')) {
41             ntext += QStringLiteral("&quot;");
42         } else {
43             ntext += c;
44         }
45     }
46 
47     return ntext;
48 }
49 
50 // Truncates the string, for output of long messages.
51 // (But don't truncate too much otherwise it's impossible to determine
52 // which message is faulty if many messages have the same beginning).
shorten(const QString & str)53 static QString shorten(const QString &str)
54 {
55     const int maxlen = 80;
56     if (str.length() <= maxlen) {
57         return str;
58     } else {
59         return QStringView(str).left(maxlen) + QSL("...");
60     }
61 }
62 
parseUiMarker(const QString & context_,QString & roleName,QString & cueName,QString & formatName)63 static void parseUiMarker(const QString &context_, QString &roleName, QString &cueName, QString &formatName)
64 {
65     // UI marker is in the form @role:cue/format,
66     // and must start just after any leading whitespace in the context string.
67     // Note that names remain untouched if the marker is not found.
68     // Normalize the whole string, all lowercase.
69     QString context = context_.trimmed().toLower();
70     if (context.startsWith(QL1C('@'))) { // found UI marker
71         static const QRegularExpression wsRx(QStringLiteral("\\s"));
72         context = context.mid(1, wsRx.match(context).capturedStart(0) - 1);
73 
74         // Possible format.
75         int pfmt = context.indexOf(QL1C('/'));
76         if (pfmt >= 0) {
77             formatName = context.mid(pfmt + 1);
78             context.truncate(pfmt);
79         }
80 
81         // Possible subcue.
82         int pcue = context.indexOf(QL1C(':'));
83         if (pcue >= 0) {
84             cueName = context.mid(pcue + 1);
85             context.truncate(pcue);
86         }
87 
88         // Role.
89         roleName = context;
90     }
91 }
92 
93 // Custom entity resolver for QXmlStreamReader.
94 class KuitEntityResolver : public QXmlStreamEntityResolver
95 {
96 public:
setEntities(const QHash<QString,QString> & entities)97     void setEntities(const QHash<QString, QString> &entities)
98     {
99         entityMap = entities;
100     }
101 
resolveUndeclaredEntity(const QString & name)102     QString resolveUndeclaredEntity(const QString &name) override
103     {
104         QString value = entityMap.value(name);
105         // This will return empty string if the entity name is not known,
106         // which will make QXmlStreamReader signal unknown entity error.
107         return value;
108     }
109 
110 private:
111     QHash<QString, QString> entityMap;
112 };
113 
114 namespace Kuit
115 {
116 enum Role { // UI marker roles
117     UndefinedRole,
118     ActionRole,
119     TitleRole,
120     OptionRole,
121     LabelRole,
122     ItemRole,
123     InfoRole,
124 };
125 
126 enum Cue { // UI marker subcues
127     UndefinedCue,
128     ButtonCue,
129     InmenuCue,
130     IntoolbarCue,
131     WindowCue,
132     MenuCue,
133     TabCue,
134     GroupCue,
135     ColumnCue,
136     RowCue,
137     SliderCue,
138     SpinboxCue,
139     ListboxCue,
140     TextboxCue,
141     ChooserCue,
142     CheckCue,
143     RadioCue,
144     InlistboxCue,
145     IntableCue,
146     InrangeCue,
147     IntextCue,
148     ValuesuffixCue,
149     TooltipCue,
150     WhatsthisCue,
151     PlaceholderCue,
152     StatusCue,
153     ProgressCue,
154     TipofthedayCue,
155     CreditCue,
156     ShellCue,
157 };
158 }
159 
160 class KuitStaticData
161 {
162 public:
163     QHash<QString, QString> xmlEntities;
164     QHash<QString, QString> xmlEntitiesInverse;
165     KuitEntityResolver xmlEntityResolver;
166 
167     QHash<QString, Kuit::Role> rolesByName;
168     QHash<QString, Kuit::Cue> cuesByName;
169     QHash<QString, Kuit::VisualFormat> formatsByName;
170     QHash<Kuit::VisualFormat, QString> namesByFormat;
171     QHash<Kuit::Role, QSet<Kuit::Cue>> knownRoleCues;
172 
173     QHash<Kuit::VisualFormat, KLocalizedString> comboKeyDelim;
174     QHash<Kuit::VisualFormat, KLocalizedString> guiPathDelim;
175     QHash<QString, KLocalizedString> keyNames;
176 
177     QHash<QByteArray, KuitSetup *> domainSetups;
178 
179     KuitStaticData();
180     ~KuitStaticData();
181 
182     KuitStaticData(const KuitStaticData &) = delete;
183     KuitStaticData &operator=(const KuitStaticData &) = delete;
184 
185     void setXmlEntityData();
186 
187     void setUiMarkerData();
188 
189     void setKeyName(const KLazyLocalizedString &keyName);
190     void setTextTransformData();
191     QString toKeyCombo(const QStringList &languages, const QString &shstr, Kuit::VisualFormat format);
192     QString toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format);
193 };
194 
KuitStaticData()195 KuitStaticData::KuitStaticData()
196 {
197     setXmlEntityData();
198     setUiMarkerData();
199     setTextTransformData();
200 }
201 
~KuitStaticData()202 KuitStaticData::~KuitStaticData()
203 {
204     qDeleteAll(domainSetups);
205 }
206 
setXmlEntityData()207 void KuitStaticData::setXmlEntityData()
208 {
209     QString LT = QStringLiteral("lt");
210     QString GT = QStringLiteral("gt");
211     QString AMP = QStringLiteral("amp");
212     QString APOS = QStringLiteral("apos");
213     QString QUOT = QStringLiteral("quot");
214 
215     // Default XML entities, direct and inverse mapping.
216     xmlEntities[LT] = QString(QL1C('<'));
217     xmlEntities[GT] = QString(QL1C('>'));
218     xmlEntities[AMP] = QString(QL1C('&'));
219     xmlEntities[APOS] = QString(QL1C('\''));
220     xmlEntities[QUOT] = QString(QL1C('"'));
221     xmlEntitiesInverse[QString(QL1C('<'))] = LT;
222     xmlEntitiesInverse[QString(QL1C('>'))] = GT;
223     xmlEntitiesInverse[QString(QL1C('&'))] = AMP;
224     xmlEntitiesInverse[QString(QL1C('\''))] = APOS;
225     xmlEntitiesInverse[QString(QL1C('"'))] = QUOT;
226 
227     // Custom XML entities.
228     xmlEntities[QStringLiteral("nbsp")] = QString(QChar(0xa0));
229 
230     xmlEntityResolver.setEntities(xmlEntities);
231 }
232 // clang-format off
setUiMarkerData()233 void KuitStaticData::setUiMarkerData()
234 {
235     using namespace Kuit;
236 
237     // Role names and their available subcues.
238 #undef SET_ROLE
239 #define SET_ROLE(role, name, cues) do { \
240         rolesByName[name] = role; \
241         knownRoleCues[role] << cues; \
242     } while (0)
243     SET_ROLE(ActionRole, QStringLiteral("action"),
244              ButtonCue << InmenuCue << IntoolbarCue);
245     SET_ROLE(TitleRole,  QStringLiteral("title"),
246              WindowCue << MenuCue << TabCue << GroupCue
247              << ColumnCue << RowCue);
248     SET_ROLE(LabelRole,  QStringLiteral("label"),
249              SliderCue << SpinboxCue << ListboxCue << TextboxCue
250              << ChooserCue);
251     SET_ROLE(OptionRole, QStringLiteral("option"),
252              CheckCue << RadioCue);
253     SET_ROLE(ItemRole,   QStringLiteral("item"),
254              InmenuCue << InlistboxCue << IntableCue << InrangeCue
255              << IntextCue << ValuesuffixCue);
256     SET_ROLE(InfoRole,   QStringLiteral("info"),
257              TooltipCue << WhatsthisCue << PlaceholderCue << StatusCue << ProgressCue
258              << TipofthedayCue << CreditCue << ShellCue);
259 
260     // Cue names.
261 #undef SET_CUE
262 #define SET_CUE(cue, name) do { \
263         cuesByName[name] = cue; \
264     } while (0)
265     SET_CUE(ButtonCue, QStringLiteral("button"));
266     SET_CUE(InmenuCue, QStringLiteral("inmenu"));
267     SET_CUE(IntoolbarCue, QStringLiteral("intoolbar"));
268     SET_CUE(WindowCue, QStringLiteral("window"));
269     SET_CUE(MenuCue, QStringLiteral("menu"));
270     SET_CUE(TabCue, QStringLiteral("tab"));
271     SET_CUE(GroupCue, QStringLiteral("group"));
272     SET_CUE(ColumnCue, QStringLiteral("column"));
273     SET_CUE(RowCue, QStringLiteral("row"));
274     SET_CUE(SliderCue, QStringLiteral("slider"));
275     SET_CUE(SpinboxCue, QStringLiteral("spinbox"));
276     SET_CUE(ListboxCue, QStringLiteral("listbox"));
277     SET_CUE(TextboxCue, QStringLiteral("textbox"));
278     SET_CUE(ChooserCue, QStringLiteral("chooser"));
279     SET_CUE(CheckCue, QStringLiteral("check"));
280     SET_CUE(RadioCue, QStringLiteral("radio"));
281     SET_CUE(InlistboxCue, QStringLiteral("inlistbox"));
282     SET_CUE(IntableCue, QStringLiteral("intable"));
283     SET_CUE(InrangeCue, QStringLiteral("inrange"));
284     SET_CUE(IntextCue, QStringLiteral("intext"));
285     SET_CUE(ValuesuffixCue, QStringLiteral("valuesuffix"));
286     SET_CUE(TooltipCue, QStringLiteral("tooltip"));
287     SET_CUE(WhatsthisCue, QStringLiteral("whatsthis"));
288     SET_CUE(PlaceholderCue, QStringLiteral("placeholder"));
289     SET_CUE(StatusCue, QStringLiteral("status"));
290     SET_CUE(ProgressCue, QStringLiteral("progress"));
291     SET_CUE(TipofthedayCue, QStringLiteral("tipoftheday"));
292     SET_CUE(CreditCue, QStringLiteral("credit"));
293     SET_CUE(ShellCue, QStringLiteral("shell"));
294 
295     // Format names.
296 #undef SET_FORMAT
297 #define SET_FORMAT(format, name) do { \
298         formatsByName[name] = format; \
299         namesByFormat[format] = name; \
300     } while (0)
301     SET_FORMAT(UndefinedFormat, QStringLiteral("undefined"));
302     SET_FORMAT(PlainText, QStringLiteral("plain"));
303     SET_FORMAT(RichText, QStringLiteral("rich"));
304     SET_FORMAT(TermText, QStringLiteral("term"));
305 }
306 
setKeyName(const KLazyLocalizedString & keyName)307 void KuitStaticData::setKeyName(const KLazyLocalizedString &keyName)
308 {
309     QString normname = QString::fromUtf8(keyName.untranslatedText()).trimmed().toLower();
310     keyNames[normname] = keyName;
311 }
312 
setTextTransformData()313 void KuitStaticData::setTextTransformData()
314 {
315     // i18n: Decide which string is used to delimit keys in a keyboard
316     // shortcut (e.g. + in Ctrl+Alt+Tab) in plain text.
317     comboKeyDelim[Kuit::PlainText] = ki18nc("shortcut-key-delimiter/plain", "+");
318     comboKeyDelim[Kuit::TermText] = comboKeyDelim[Kuit::PlainText];
319     // i18n: Decide which string is used to delimit keys in a keyboard
320     // shortcut (e.g. + in Ctrl+Alt+Tab) in rich text.
321     comboKeyDelim[Kuit::RichText] = ki18nc("shortcut-key-delimiter/rich", "+");
322 
323     // i18n: Decide which string is used to delimit elements in a GUI path
324     // (e.g. -> in "Go to Settings->Advanced->Core tab.") in plain text.
325     guiPathDelim[Kuit::PlainText] = ki18nc("gui-path-delimiter/plain", "→");
326     guiPathDelim[Kuit::TermText] = guiPathDelim[Kuit::PlainText];
327     // i18n: Decide which string is used to delimit elements in a GUI path
328     // (e.g. -> in "Go to Settings->Advanced->Core tab.") in rich text.
329     guiPathDelim[Kuit::RichText] = ki18nc("gui-path-delimiter/rich", "→");
330     // NOTE: The '→' glyph seems to be available in all widespread fonts.
331 
332     // Collect keyboard key names.
333     setKeyName(kli18nc("keyboard-key-name", "Alt"));
334     setKeyName(kli18nc("keyboard-key-name", "AltGr"));
335     setKeyName(kli18nc("keyboard-key-name", "Backspace"));
336     setKeyName(kli18nc("keyboard-key-name", "CapsLock"));
337     setKeyName(kli18nc("keyboard-key-name", "Control"));
338     setKeyName(kli18nc("keyboard-key-name", "Ctrl"));
339     setKeyName(kli18nc("keyboard-key-name", "Del"));
340     setKeyName(kli18nc("keyboard-key-name", "Delete"));
341     setKeyName(kli18nc("keyboard-key-name", "Down"));
342     setKeyName(kli18nc("keyboard-key-name", "End"));
343     setKeyName(kli18nc("keyboard-key-name", "Enter"));
344     setKeyName(kli18nc("keyboard-key-name", "Esc"));
345     setKeyName(kli18nc("keyboard-key-name", "Escape"));
346     setKeyName(kli18nc("keyboard-key-name", "Home"));
347     setKeyName(kli18nc("keyboard-key-name", "Hyper"));
348     setKeyName(kli18nc("keyboard-key-name", "Ins"));
349     setKeyName(kli18nc("keyboard-key-name", "Insert"));
350     setKeyName(kli18nc("keyboard-key-name", "Left"));
351     setKeyName(kli18nc("keyboard-key-name", "Menu"));
352     setKeyName(kli18nc("keyboard-key-name", "Meta"));
353     setKeyName(kli18nc("keyboard-key-name", "NumLock"));
354     setKeyName(kli18nc("keyboard-key-name", "PageDown"));
355     setKeyName(kli18nc("keyboard-key-name", "PageUp"));
356     setKeyName(kli18nc("keyboard-key-name", "PgDown"));
357     setKeyName(kli18nc("keyboard-key-name", "PgUp"));
358     setKeyName(kli18nc("keyboard-key-name", "PauseBreak"));
359     setKeyName(kli18nc("keyboard-key-name", "PrintScreen"));
360     setKeyName(kli18nc("keyboard-key-name", "PrtScr"));
361     setKeyName(kli18nc("keyboard-key-name", "Return"));
362     setKeyName(kli18nc("keyboard-key-name", "Right"));
363     setKeyName(kli18nc("keyboard-key-name", "ScrollLock"));
364     setKeyName(kli18nc("keyboard-key-name", "Shift"));
365     setKeyName(kli18nc("keyboard-key-name", "Space"));
366     setKeyName(kli18nc("keyboard-key-name", "Super"));
367     setKeyName(kli18nc("keyboard-key-name", "SysReq"));
368     setKeyName(kli18nc("keyboard-key-name", "Tab"));
369     setKeyName(kli18nc("keyboard-key-name", "Up"));
370     setKeyName(kli18nc("keyboard-key-name", "Win"));
371     setKeyName(kli18nc("keyboard-key-name", "F1"));
372     setKeyName(kli18nc("keyboard-key-name", "F2"));
373     setKeyName(kli18nc("keyboard-key-name", "F3"));
374     setKeyName(kli18nc("keyboard-key-name", "F4"));
375     setKeyName(kli18nc("keyboard-key-name", "F5"));
376     setKeyName(kli18nc("keyboard-key-name", "F6"));
377     setKeyName(kli18nc("keyboard-key-name", "F7"));
378     setKeyName(kli18nc("keyboard-key-name", "F8"));
379     setKeyName(kli18nc("keyboard-key-name", "F9"));
380     setKeyName(kli18nc("keyboard-key-name", "F10"));
381     setKeyName(kli18nc("keyboard-key-name", "F11"));
382     setKeyName(kli18nc("keyboard-key-name", "F12"));
383     // TODO: Add rest of the key names?
384 }
385 // clang-format on
386 
toKeyCombo(const QStringList & languages,const QString & shstr,Kuit::VisualFormat format)387 QString KuitStaticData::toKeyCombo(const QStringList &languages, const QString &shstr, Kuit::VisualFormat format)
388 {
389     // Take '+' or '-' as input shortcut delimiter,
390     // whichever is first encountered.
391     static const QRegularExpression delimRx(QStringLiteral("[+-]"));
392 
393     const QRegularExpressionMatch match = delimRx.match(shstr);
394     QStringList keys;
395     if (match.hasMatch()) { // delimiter found, multi-key shortcut
396         const QString oldDelim = match.captured(0);
397         keys = shstr.split(oldDelim, Qt::SkipEmptyParts);
398     } else { // single-key shortcut, no delimiter found
399         keys.append(shstr);
400     }
401 
402     for (int i = 0; i < keys.size(); ++i) {
403         // Normalize key, trim and all lower-case.
404         const QString nkey = keys.at(i).trimmed().toLower();
405         keys[i] = keyNames.contains(nkey) ? keyNames[nkey].toString(languages) : keys.at(i).trimmed();
406     }
407     const QString delim = comboKeyDelim.value(format).toString(languages);
408     return keys.join(delim);
409 }
410 
toInterfacePath(const QStringList & languages,const QString & inpstr,Kuit::VisualFormat format)411 QString KuitStaticData::toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format)
412 {
413     // Take '/', '|' or "->" as input path delimiter,
414     // whichever is first encountered.
415     static const QRegularExpression delimRx(QStringLiteral("\\||->"));
416     const QRegularExpressionMatch match = delimRx.match(inpstr);
417     if (match.hasMatch()) { // multi-element path
418         const QString oldDelim = match.captured(0);
419         QStringList guiels = inpstr.split(oldDelim, Qt::SkipEmptyParts);
420         const QString delim = guiPathDelim.value(format).toString(languages);
421         return guiels.join(delim);
422     }
423 
424     // single-element path, no delimiter found
425     return inpstr;
426 }
427 
Q_GLOBAL_STATIC(KuitStaticData,staticData)428 Q_GLOBAL_STATIC(KuitStaticData, staticData)
429 
430 static QString attributeSetKey(const QStringList &attribNames_)
431 {
432     QStringList attribNames = attribNames_;
433     std::sort(attribNames.begin(), attribNames.end());
434     QString key = QL1C('[') + attribNames.join(QL1C(' ')) + QL1C(']');
435     return key;
436 }
437 
438 class KuitTag
439 {
440 public:
441     QString name;
442     Kuit::TagClass type;
443     QSet<QString> knownAttribs;
444     QHash<QString, QHash<Kuit::VisualFormat, QStringList>> attributeOrders;
445     QHash<QString, QHash<Kuit::VisualFormat, KLocalizedString>> patterns;
446     QHash<QString, QHash<Kuit::VisualFormat, Kuit::TagFormatter>> formatters;
447     int leadingNewlines;
448     QString format(const QStringList &languages,
449                    const QHash<QString, QString> &attributes,
450                    const QString &text,
451                    const QStringList &tagPath,
452                    Kuit::VisualFormat format) const;
453 };
454 
format(const QStringList & languages,const QHash<QString,QString> & attributes,const QString & text,const QStringList & tagPath,Kuit::VisualFormat format) const455 QString KuitTag::format(const QStringList &languages,
456                         const QHash<QString, QString> &attributes,
457                         const QString &text,
458                         const QStringList &tagPath,
459                         Kuit::VisualFormat format) const
460 {
461     KuitStaticData *s = staticData();
462     QString formattedText = text;
463     QString attribKey = attributeSetKey(attributes.keys());
464     const QHash<Kuit::VisualFormat, KLocalizedString> pattern = patterns.value(attribKey);
465     if (pattern.contains(format)) {
466         QString modText;
467         Kuit::TagFormatter formatter = formatters.value(attribKey).value(format);
468         if (formatter != nullptr) {
469             modText = formatter(languages, name, attributes, text, tagPath, format);
470         } else {
471             modText = text;
472         }
473         KLocalizedString aggText = pattern.value(format);
474         // line below is first-aid fix.for e.g. <emphasis strong='true'>.
475         // TODO: proper handling of boolean attributes still needed
476         aggText = aggText.relaxSubs();
477         if (!aggText.isEmpty()) {
478             aggText = aggText.subs(modText);
479             const QStringList attributeOrder = attributeOrders.value(attribKey).value(format);
480             for (const QString &attribName : attributeOrder) {
481                 aggText = aggText.subs(attributes.value(attribName));
482             }
483             formattedText = aggText.ignoreMarkup().toString(languages);
484         } else {
485             formattedText = modText;
486         }
487     } else if (patterns.contains(attribKey)) {
488         qCWarning(KI18N_KUIT)
489             << QStringLiteral("Undefined visual format for tag <%1> and attribute combination %2: %3.").arg(name, attribKey, s->namesByFormat.value(format));
490     } else {
491         qCWarning(KI18N_KUIT) << QStringLiteral("Undefined attribute combination for tag <%1>: %2.").arg(name, attribKey);
492     }
493     return formattedText;
494 }
495 
setupForDomain(const QByteArray & domain)496 KuitSetup &Kuit::setupForDomain(const QByteArray &domain)
497 {
498     KuitStaticData *s = staticData();
499     KuitSetup *setup = s->domainSetups.value(domain);
500     if (!setup) {
501         setup = new KuitSetup(domain);
502         s->domainSetups.insert(domain, setup);
503     }
504     return *setup;
505 }
506 
setupForDomain(const char * domain)507 KuitSetup &Kuit::setupForDomain(const char *domain)
508 {
509     return setupForDomain(QByteArray(domain));
510 }
511 
512 class KuitSetupPrivate
513 {
514 public:
515     void setTagPattern(const QString &tagName,
516                        const QStringList &attribNames,
517                        Kuit::VisualFormat format,
518                        const KLocalizedString &pattern,
519                        Kuit::TagFormatter formatter,
520                        int leadingNewlines);
521 
522     void setTagClass(const QString &tagName, Kuit::TagClass aClass);
523 
524     void setFormatForMarker(const QString &marker, Kuit::VisualFormat format);
525 
526     void setDefaultMarkup();
527     void setDefaultFormats();
528 
529     QByteArray domain;
530     QHash<QString, KuitTag> knownTags;
531     QHash<Kuit::Role, QHash<Kuit::Cue, Kuit::VisualFormat>> formatsByRoleCue;
532 };
533 
setTagPattern(const QString & tagName,const QStringList & attribNames_,Kuit::VisualFormat format,const KLocalizedString & pattern,Kuit::TagFormatter formatter,int leadingNewlines_)534 void KuitSetupPrivate::setTagPattern(const QString &tagName,
535                                      const QStringList &attribNames_,
536                                      Kuit::VisualFormat format,
537                                      const KLocalizedString &pattern,
538                                      Kuit::TagFormatter formatter,
539                                      int leadingNewlines_)
540 {
541     bool isNewTag = knownTags.contains(tagName);
542     KuitTag &tag = knownTags[tagName];
543     if (isNewTag) {
544         tag.name = tagName;
545         tag.type = Kuit::PhraseTag;
546     }
547     QStringList attribNames = attribNames_;
548     attribNames.removeAll(QString());
549     for (const QString &attribName : std::as_const(attribNames)) {
550         tag.knownAttribs.insert(attribName);
551     }
552     QString attribKey = attributeSetKey(attribNames);
553     tag.attributeOrders[attribKey][format] = attribNames;
554     tag.patterns[attribKey][format] = pattern;
555     tag.formatters[attribKey][format] = formatter;
556     tag.leadingNewlines = leadingNewlines_;
557 }
558 
setTagClass(const QString & tagName,Kuit::TagClass aClass)559 void KuitSetupPrivate::setTagClass(const QString &tagName, Kuit::TagClass aClass)
560 {
561     bool isNewTag = knownTags.contains(tagName);
562     KuitTag &tag = knownTags[tagName];
563     if (isNewTag) {
564         tag.name = tagName;
565     }
566     tag.type = aClass;
567 }
568 
setFormatForMarker(const QString & marker,Kuit::VisualFormat format)569 void KuitSetupPrivate::setFormatForMarker(const QString &marker, Kuit::VisualFormat format)
570 {
571     KuitStaticData *s = staticData();
572 
573     QString roleName;
574     QString cueName;
575     QString formatName;
576     parseUiMarker(marker, roleName, cueName, formatName);
577 
578     Kuit::Role role;
579     if (s->rolesByName.contains(roleName)) {
580         role = s->rolesByName.value(roleName);
581     } else if (!roleName.isEmpty()) {
582         qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker {%2}, visual format not set.").arg(roleName, marker);
583         return;
584     } else {
585         qCWarning(KI18N_KUIT) << QStringLiteral("Empty role in UI marker {%1}, visual format not set.").arg(marker);
586         return;
587     }
588 
589     Kuit::Cue cue;
590     if (s->cuesByName.contains(cueName)) {
591         cue = s->cuesByName.value(cueName);
592         if (!s->knownRoleCues.value(role).contains(cue)) {
593             qCWarning(KI18N_KUIT)
594                 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker {%3}, visual format not set.").arg(cueName, roleName, marker);
595             return;
596         }
597     } else if (!cueName.isEmpty()) {
598         qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker {%2}, visual format not set.").arg(cueName, marker);
599         return;
600     } else {
601         cue = Kuit::UndefinedCue;
602     }
603 
604     formatsByRoleCue[role][cue] = format;
605 }
606 
607 #define TAG_FORMATTER_ARGS                                                                                                                                     \
608     const QStringList &languages, const QString &tagName, const QHash<QString, QString> &attributes, const QString &text, const QStringList &tagPath,          \
609         Kuit::VisualFormat format
610 
tagFormatterFilename(TAG_FORMATTER_ARGS)611 static QString tagFormatterFilename(TAG_FORMATTER_ARGS)
612 {
613     Q_UNUSED(languages);
614     Q_UNUSED(tagName);
615     Q_UNUSED(attributes);
616     Q_UNUSED(tagPath);
617 #ifdef Q_OS_WIN
618     // with rich text the path can include <foo>...</foo> which will be replaced by <foo>...<\foo> on Windows!
619     // the same problem also happens for tags such as <br/> -> <br\>
620     if (format == Kuit::RichText) {
621         // replace all occurrences of "</" or "/>" to make sure toNativeSeparators() doesn't destroy XML markup
622         const auto KUIT_CLOSE_XML_REPLACEMENT = QStringLiteral("__kuit_close_xml_tag__");
623         const auto KUIT_NOTEXT_XML_REPLACEMENT = QStringLiteral("__kuit_notext_xml_tag__");
624 
625         QString result = text;
626         result.replace(QStringLiteral("</"), KUIT_CLOSE_XML_REPLACEMENT);
627         result.replace(QStringLiteral("/>"), KUIT_NOTEXT_XML_REPLACEMENT);
628         result = QDir::toNativeSeparators(result);
629         result.replace(KUIT_CLOSE_XML_REPLACEMENT, QStringLiteral("</"));
630         result.replace(KUIT_NOTEXT_XML_REPLACEMENT, QStringLiteral("/>"));
631         return result;
632     }
633 #else
634     Q_UNUSED(format);
635 #endif
636     return QDir::toNativeSeparators(text);
637 }
638 
tagFormatterShortcut(TAG_FORMATTER_ARGS)639 static QString tagFormatterShortcut(TAG_FORMATTER_ARGS)
640 {
641     Q_UNUSED(tagName);
642     Q_UNUSED(attributes);
643     Q_UNUSED(tagPath);
644     KuitStaticData *s = staticData();
645     return s->toKeyCombo(languages, text, format);
646 }
647 
tagFormatterInterface(TAG_FORMATTER_ARGS)648 static QString tagFormatterInterface(TAG_FORMATTER_ARGS)
649 {
650     Q_UNUSED(tagName);
651     Q_UNUSED(attributes);
652     Q_UNUSED(tagPath);
653     KuitStaticData *s = staticData();
654     return s->toInterfacePath(languages, text, format);
655 }
656 
setDefaultMarkup()657 void KuitSetupPrivate::setDefaultMarkup()
658 {
659     using namespace Kuit;
660 
661     const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__");
662     const QString TITLE = QStringLiteral("title");
663     const QString EMPHASIS = QStringLiteral("emphasis");
664     const QString COMMAND = QStringLiteral("command");
665     const QString WARNING = QStringLiteral("warning");
666     const QString LINK = QStringLiteral("link");
667     const QString NOTE = QStringLiteral("note");
668 
669     // clang-format off
670     // Macro to hide message from extraction.
671 #define HI18NC ki18nc
672 
673     // Macro to expedite setting the patterns.
674 #undef SET_PATTERN
675 #define SET_PATTERN(tagName, attribNames_, format, pattern, formatter, leadNl) \
676     do { \
677         QStringList attribNames; \
678         attribNames << attribNames_; \
679         setTagPattern(tagName, attribNames, format, pattern, formatter, leadNl); \
680         /* Make TermText pattern same as PlainText if not explicitly given. */ \
681         KuitTag &tag = knownTags[tagName]; \
682         QString attribKey = attributeSetKey(attribNames); \
683         if (format == PlainText && !tag.patterns[attribKey].contains(TermText)) { \
684             setTagPattern(tagName, attribNames, TermText, pattern, formatter, leadNl); \
685         } \
686     } while (0)
687 
688     // NOTE: The following "i18n:" comments are oddly placed in order that
689     // xgettext extracts them properly.
690 
691     // -------> Internal top tag
692     setTagClass(INTERNAL_TOP_TAG_NAME, StructTag);
693     setTagClass(INTERNAL_TOP_TAG_NAME, StructTag);
694     SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), PlainText,
695                 HI18NC("tag-format-pattern <> plain",
696                        // i18n: KUIT pattern, see the comment to the first of these entries above.
697                        "%1"),
698                 nullptr, 0);
699     SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), RichText,
700                 HI18NC("tag-format-pattern <> rich",
701                        // i18n: KUIT pattern, see the comment to the first of these entries above.
702                        "%1"),
703                 nullptr, 0);
704 
705     // -------> Title
706     setTagClass(TITLE, StructTag);
707     SET_PATTERN(TITLE, QString(), PlainText,
708                 ki18nc("tag-format-pattern <title> plain",
709                        // i18n: The messages with context "tag-format-pattern <tag ...> format"
710                        // are KUIT patterns for formatting the text found inside KUIT tags.
711                        // The format is either "plain" or "rich", and tells if the pattern
712                        // is used for plain text or rich text (which can use HTML tags).
713                        // You may be in general satisfied with the patterns as they are in the
714                        // original. Some things you may consider changing:
715                        // - the proper quotes, those used in msgid are English-standard
716                        // - the <i> and <b> tags, does your language script work well with them?
717                        "== %1 =="),
718                 nullptr, 2);
719     SET_PATTERN(TITLE, QString(), RichText,
720                 ki18nc("tag-format-pattern <title> rich",
721                        // i18n: KUIT pattern, see the comment to the first of these entries above.
722                        "<h2>%1</h2>"),
723                 nullptr, 2);
724 
725     // -------> Subtitle
726     setTagClass(QSL("subtitle"), StructTag);
727     SET_PATTERN(QSL("subtitle"), QString(), PlainText,
728                 ki18nc("tag-format-pattern <subtitle> plain",
729                        // i18n: KUIT pattern, see the comment to the first of these entries above.
730                        "~ %1 ~"),
731                 nullptr, 2);
732     SET_PATTERN(QSL("subtitle"), QString(), RichText,
733                 ki18nc("tag-format-pattern <subtitle> rich",
734                        // i18n: KUIT pattern, see the comment to the first of these entries above.
735                        "<h3>%1</h3>"),
736                 nullptr, 2);
737 
738     // -------> Para
739     setTagClass(QSL("para"), StructTag);
740     SET_PATTERN(QSL("para"), QString(), PlainText,
741                 ki18nc("tag-format-pattern <para> plain",
742                        // i18n: KUIT pattern, see the comment to the first of these entries above.
743                        "%1"),
744                 nullptr, 2);
745     SET_PATTERN(QSL("para"), QString(), RichText,
746                 ki18nc("tag-format-pattern <para> rich",
747                        // i18n: KUIT pattern, see the comment to the first of these entries above.
748                        "<p>%1</p>"),
749                 nullptr, 2);
750 
751     // -------> List
752     setTagClass(QSL("list"), StructTag);
753     SET_PATTERN(QSL("list"), QString(), PlainText,
754                 ki18nc("tag-format-pattern <list> plain",
755                        // i18n: KUIT pattern, see the comment to the first of these entries above.
756                        "%1"),
757                 nullptr, 1);
758     SET_PATTERN(QSL("list"), QString(), RichText,
759                 ki18nc("tag-format-pattern <list> rich",
760                        // i18n: KUIT pattern, see the comment to the first of these entries above.
761                        "<ul>%1</ul>"),
762                 nullptr, 1);
763 
764     // -------> Item
765     setTagClass(QSL("item"), StructTag);
766     SET_PATTERN(QSL("item"), QString(), PlainText,
767                 ki18nc("tag-format-pattern <item> plain",
768                        // i18n: KUIT pattern, see the comment to the first of these entries above.
769                        "  * %1"),
770                 nullptr, 1);
771     SET_PATTERN(QSL("item"), QString(), RichText,
772                 ki18nc("tag-format-pattern <item> rich",
773                        // i18n: KUIT pattern, see the comment to the first of these entries above.
774                        "<li>%1</li>"),
775                 nullptr, 1);
776 
777     // -------> Note
778     SET_PATTERN(NOTE, QString(), PlainText,
779                 ki18nc("tag-format-pattern <note> plain",
780                        // i18n: KUIT pattern, see the comment to the first of these entries above.
781                        "Note: %1"),
782                 nullptr, 0);
783     SET_PATTERN(NOTE, QString(), RichText,
784                 ki18nc("tag-format-pattern <note> rich",
785                        // i18n: KUIT pattern, see the comment to the first of these entries above.
786                        "<i>Note</i>: %1"),
787                 nullptr, 0);
788     SET_PATTERN(NOTE, QSL("label"), PlainText,
789                 ki18nc("tag-format-pattern <note label=> plain\n"
790                        "%1 is the text, %2 is the note label",
791                        // i18n: KUIT pattern, see the comment to the first of these entries above.
792                        "%2: %1"),
793                 nullptr, 0);
794     SET_PATTERN(NOTE, QSL("label"), RichText,
795                 ki18nc("tag-format-pattern <note label=> rich\n"
796                        "%1 is the text, %2 is the note label",
797                        // i18n: KUIT pattern, see the comment to the first of these entries above.
798                        "<i>%2</i>: %1"),
799                 nullptr, 0);
800 
801     // -------> Warning
802     SET_PATTERN(WARNING, QString(), PlainText,
803                 ki18nc("tag-format-pattern <warning> plain",
804                        // i18n: KUIT pattern, see the comment to the first of these entries above.
805                        "WARNING: %1"),
806                 nullptr, 0);
807     SET_PATTERN(WARNING, QString(), RichText,
808                 ki18nc("tag-format-pattern <warning> rich",
809                        // i18n: KUIT pattern, see the comment to the first of these entries above.
810                        "<b>Warning</b>: %1"),
811                 nullptr, 0);
812     SET_PATTERN(WARNING, QSL("label"), PlainText,
813                 ki18nc("tag-format-pattern <warning label=> plain\n"
814                        "%1 is the text, %2 is the warning label",
815                        // i18n: KUIT pattern, see the comment to the first of these entries above.
816                        "%2: %1"),
817                 nullptr, 0);
818     SET_PATTERN(WARNING, QSL("label"), RichText,
819                 ki18nc("tag-format-pattern <warning label=> rich\n"
820                        "%1 is the text, %2 is the warning label",
821                        // i18n: KUIT pattern, see the comment to the first of these entries above.
822                        "<b>%2</b>: %1"),
823                 nullptr, 0);
824 
825     // -------> Link
826     SET_PATTERN(LINK, QString(), PlainText,
827                 ki18nc("tag-format-pattern <link> plain",
828                        // i18n: KUIT pattern, see the comment to the first of these entries above.
829                        "%1"),
830                 nullptr, 0);
831     SET_PATTERN(LINK, QString(), RichText,
832                 ki18nc("tag-format-pattern <link> rich",
833                        // i18n: KUIT pattern, see the comment to the first of these entries above.
834                        "<a href=\"%1\">%1</a>"),
835                 nullptr, 0);
836     SET_PATTERN(LINK, QSL("url"), PlainText,
837                 ki18nc("tag-format-pattern <link url=> plain\n"
838                        "%1 is the descriptive text, %2 is the URL",
839                        // i18n: KUIT pattern, see the comment to the first of these entries above.
840                        "%1 (%2)"),
841                 nullptr, 0);
842     SET_PATTERN(LINK, QSL("url"), RichText,
843                 ki18nc("tag-format-pattern <link url=> rich\n"
844                        "%1 is the descriptive text, %2 is the URL",
845                        // i18n: KUIT pattern, see the comment to the first of these entries above.
846                        "<a href=\"%2\">%1</a>"),
847                 nullptr, 0);
848 
849     // -------> Filename
850     SET_PATTERN(QSL("filename"), QString(), PlainText,
851                 ki18nc("tag-format-pattern <filename> plain",
852                        // i18n: KUIT pattern, see the comment to the first of these entries above.
853                        "‘%1’"),
854                 tagFormatterFilename, 0);
855     SET_PATTERN(QSL("filename"), QString(), RichText,
856                 ki18nc("tag-format-pattern <filename> rich",
857                        // i18n: KUIT pattern, see the comment to the first of these entries above.
858                        "‘<tt>%1</tt>’"),
859                 tagFormatterFilename, 0);
860 
861     // -------> Application
862     SET_PATTERN(QSL("application"), QString(), PlainText,
863                 ki18nc("tag-format-pattern <application> plain",
864                        // i18n: KUIT pattern, see the comment to the first of these entries above.
865                        "%1"),
866                 nullptr, 0);
867     SET_PATTERN(QSL("application"), QString(), RichText,
868                 ki18nc("tag-format-pattern <application> rich",
869                        // i18n: KUIT pattern, see the comment to the first of these entries above.
870                        "%1"),
871                 nullptr, 0);
872 
873     // -------> Command
874     SET_PATTERN(COMMAND, QString(), PlainText,
875                 ki18nc("tag-format-pattern <command> plain",
876                        // i18n: KUIT pattern, see the comment to the first of these entries above.
877                        "%1"),
878                 nullptr, 0);
879     SET_PATTERN(COMMAND, QString(), RichText,
880                 ki18nc("tag-format-pattern <command> rich",
881                        // i18n: KUIT pattern, see the comment to the first of these entries above.
882                        "<tt>%1</tt>"),
883                 nullptr, 0);
884     SET_PATTERN(COMMAND, QSL("section"), PlainText,
885                 ki18nc("tag-format-pattern <command section=> plain\n"
886                        "%1 is the command name, %2 is its man section",
887                        // i18n: KUIT pattern, see the comment to the first of these entries above.
888                        "%1(%2)"),
889                 nullptr, 0);
890     SET_PATTERN(COMMAND, QSL("section"), RichText,
891                 ki18nc("tag-format-pattern <command section=> rich\n"
892                        "%1 is the command name, %2 is its man section",
893                        // i18n: KUIT pattern, see the comment to the first of these entries above.
894                        "<tt>%1(%2)</tt>"),
895                 nullptr, 0);
896 
897     // -------> Resource
898     SET_PATTERN(QSL("resource"), QString(), PlainText,
899                 ki18nc("tag-format-pattern <resource> plain",
900                        // i18n: KUIT pattern, see the comment to the first of these entries above.
901                        "“%1”"),
902                 nullptr, 0);
903     SET_PATTERN(QSL("resource"), QString(), RichText,
904                 ki18nc("tag-format-pattern <resource> rich",
905                        // i18n: KUIT pattern, see the comment to the first of these entries above.
906                        "“%1”"),
907                 nullptr, 0);
908 
909     // -------> Icode
910     SET_PATTERN(QSL("icode"), QString(), PlainText,
911                 ki18nc("tag-format-pattern <icode> plain",
912                        // i18n: KUIT pattern, see the comment to the first of these entries above.
913                        "“%1”"),
914                 nullptr, 0);
915     SET_PATTERN(QSL("icode"), QString(), RichText,
916                 ki18nc("tag-format-pattern <icode> rich",
917                        // i18n: KUIT pattern, see the comment to the first of these entries above.
918                        "<tt>%1</tt>"),
919                 nullptr, 0);
920 
921     // -------> Bcode
922     SET_PATTERN(QSL("bcode"), QString(), PlainText,
923                 ki18nc("tag-format-pattern <bcode> plain",
924                        // i18n: KUIT pattern, see the comment to the first of these entries above.
925                        "\n%1\n"),
926                 nullptr, 2);
927     SET_PATTERN(QSL("bcode"), QString(), RichText,
928                 ki18nc("tag-format-pattern <bcode> rich",
929                        // i18n: KUIT pattern, see the comment to the first of these entries above.
930                        "<pre>%1</pre>"),
931                 nullptr, 2);
932 
933     // -------> Shortcut
934     SET_PATTERN(QSL("shortcut"), QString(), PlainText,
935                 ki18nc("tag-format-pattern <shortcut> plain",
936                        // i18n: KUIT pattern, see the comment to the first of these entries above.
937                        "%1"),
938                 tagFormatterShortcut, 0);
939     SET_PATTERN(QSL("shortcut"), QString(), RichText,
940                 ki18nc("tag-format-pattern <shortcut> rich",
941                        // i18n: KUIT pattern, see the comment to the first of these entries above.
942                        "<b>%1</b>"),
943                 tagFormatterShortcut, 0);
944 
945     // -------> Interface
946     SET_PATTERN(QSL("interface"), QString(), PlainText,
947                 ki18nc("tag-format-pattern <interface> plain",
948                        // i18n: KUIT pattern, see the comment to the first of these entries above.
949                        "|%1|"),
950                 tagFormatterInterface, 0);
951     SET_PATTERN(QSL("interface"), QString(), RichText,
952                 ki18nc("tag-format-pattern <interface> rich",
953                        // i18n: KUIT pattern, see the comment to the first of these entries above.
954                        "<i>%1</i>"),
955                 tagFormatterInterface, 0);
956 
957     // -------> Emphasis
958     SET_PATTERN(EMPHASIS, QString(), PlainText,
959                 ki18nc("tag-format-pattern <emphasis> plain",
960                        // i18n: KUIT pattern, see the comment to the first of these entries above.
961                        "*%1*"),
962                 nullptr, 0);
963     SET_PATTERN(EMPHASIS, QString(), RichText,
964                 ki18nc("tag-format-pattern <emphasis> rich",
965                        // i18n: KUIT pattern, see the comment to the first of these entries above.
966                        "<i>%1</i>"),
967                 nullptr, 0);
968     SET_PATTERN(EMPHASIS, QSL("strong"), PlainText,
969                 ki18nc("tag-format-pattern <emphasis-strong> plain",
970                        // i18n: KUIT pattern, see the comment to the first of these entries above.
971                        "**%1**"),
972                 nullptr, 0);
973     SET_PATTERN(EMPHASIS, QSL("strong"), RichText,
974                 ki18nc("tag-format-pattern <emphasis-strong> rich",
975                        // i18n: KUIT pattern, see the comment to the first of these entries above.
976                        "<b>%1</b>"),
977                 nullptr, 0);
978 
979     // -------> Placeholder
980     SET_PATTERN(QSL("placeholder"), QString(), PlainText,
981                 ki18nc("tag-format-pattern <placeholder> plain",
982                        // i18n: KUIT pattern, see the comment to the first of these entries above.
983                        "&lt;%1&gt;"),
984                 nullptr, 0);
985     SET_PATTERN(QSL("placeholder"), QString(), RichText,
986                 ki18nc("tag-format-pattern <placeholder> rich",
987                        // i18n: KUIT pattern, see the comment to the first of these entries above.
988                        "&lt;<i>%1</i>&gt;"),
989                 nullptr, 0);
990 
991     // -------> Email
992     SET_PATTERN(QSL("email"), QString(), PlainText,
993                 ki18nc("tag-format-pattern <email> plain",
994                        // i18n: KUIT pattern, see the comment to the first of these entries above.
995                        "&lt;%1&gt;"),
996                 nullptr, 0);
997     SET_PATTERN(QSL("email"), QString(), RichText,
998                 ki18nc("tag-format-pattern <email> rich",
999                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1000                        "&lt;<a href=\"mailto:%1\">%1</a>&gt;"),
1001                 nullptr, 0);
1002     SET_PATTERN(QSL("email"), QSL("address"), PlainText,
1003                 ki18nc("tag-format-pattern <email address=> plain\n"
1004                        "%1 is name, %2 is address",
1005                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1006                        "%1 &lt;%2&gt;"),
1007                 nullptr, 0);
1008     SET_PATTERN(QSL("email"), QSL("address"), RichText,
1009                 ki18nc("tag-format-pattern <email address=> rich\n"
1010                        "%1 is name, %2 is address",
1011                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1012                        "<a href=\"mailto:%2\">%1</a>"),
1013                 nullptr, 0);
1014 
1015     // -------> Envar
1016     SET_PATTERN(QSL("envar"), QString(), PlainText,
1017                 ki18nc("tag-format-pattern <envar> plain",
1018                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1019                        "$%1"),
1020                 nullptr, 0);
1021     SET_PATTERN(QSL("envar"), QString(), RichText,
1022                 ki18nc("tag-format-pattern <envar> rich",
1023                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1024                        "<tt>$%1</tt>"),
1025                 nullptr, 0);
1026 
1027     // -------> Message
1028     SET_PATTERN(QSL("message"), QString(), PlainText,
1029                 ki18nc("tag-format-pattern <message> plain",
1030                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1031                        "/%1/"),
1032                 nullptr, 0);
1033     SET_PATTERN(QSL("message"), QString(), RichText,
1034                 ki18nc("tag-format-pattern <message> rich",
1035                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1036                        "<i>%1</i>"),
1037                 nullptr, 0);
1038 
1039     // -------> Nl
1040     SET_PATTERN(QSL("nl"), QString(), PlainText,
1041                 ki18nc("tag-format-pattern <nl> plain",
1042                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1043                        "%1\n"),
1044                 nullptr, 0);
1045     SET_PATTERN(QSL("nl"), QString(), RichText,
1046                 ki18nc("tag-format-pattern <nl> rich",
1047                        // i18n: KUIT pattern, see the comment to the first of these entries above.
1048                        "%1<br/>"),
1049                 nullptr, 0);
1050     // clang-format on
1051 }
1052 
setDefaultFormats()1053 void KuitSetupPrivate::setDefaultFormats()
1054 {
1055     using namespace Kuit;
1056 
1057     // Setup formats by role.
1058     formatsByRoleCue[ActionRole][UndefinedCue] = PlainText;
1059     formatsByRoleCue[TitleRole][UndefinedCue] = PlainText;
1060     formatsByRoleCue[LabelRole][UndefinedCue] = PlainText;
1061     formatsByRoleCue[OptionRole][UndefinedCue] = PlainText;
1062     formatsByRoleCue[ItemRole][UndefinedCue] = PlainText;
1063     formatsByRoleCue[InfoRole][UndefinedCue] = RichText;
1064 
1065     // Setup override formats by subcue.
1066     formatsByRoleCue[InfoRole][StatusCue] = PlainText;
1067     formatsByRoleCue[InfoRole][ProgressCue] = PlainText;
1068     formatsByRoleCue[InfoRole][CreditCue] = PlainText;
1069     formatsByRoleCue[InfoRole][ShellCue] = TermText;
1070 }
1071 
KuitSetup(const QByteArray & domain)1072 KuitSetup::KuitSetup(const QByteArray &domain)
1073     : d(new KuitSetupPrivate)
1074 {
1075     d->domain = domain;
1076     d->setDefaultMarkup();
1077     d->setDefaultFormats();
1078 }
1079 
1080 KuitSetup::~KuitSetup() = default;
1081 
setTagPattern(const QString & tagName,const QStringList & attribNames,Kuit::VisualFormat format,const KLocalizedString & pattern,Kuit::TagFormatter formatter,int leadingNewlines)1082 void KuitSetup::setTagPattern(const QString &tagName,
1083                               const QStringList &attribNames,
1084                               Kuit::VisualFormat format,
1085                               const KLocalizedString &pattern,
1086                               Kuit::TagFormatter formatter,
1087                               int leadingNewlines)
1088 {
1089     d->setTagPattern(tagName, attribNames, format, pattern, formatter, leadingNewlines);
1090 }
1091 
setTagClass(const QString & tagName,Kuit::TagClass aClass)1092 void KuitSetup::setTagClass(const QString &tagName, Kuit::TagClass aClass)
1093 {
1094     d->setTagClass(tagName, aClass);
1095 }
1096 
setFormatForMarker(const QString & marker,Kuit::VisualFormat format)1097 void KuitSetup::setFormatForMarker(const QString &marker, Kuit::VisualFormat format)
1098 {
1099     d->setFormatForMarker(marker, format);
1100 }
1101 
1102 class KuitFormatterPrivate
1103 {
1104 public:
1105     KuitFormatterPrivate(const QString &language);
1106 
1107     QString format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const;
1108 
1109     // Get metatranslation (formatting patterns, etc.)
1110     QString metaTr(const char *context, const char *text) const;
1111 
1112     // Set visual formatting patterns for text within tags.
1113     void setFormattingPatterns();
1114 
1115     // Set data used in transformation of text within tags.
1116     void setTextTransformData();
1117 
1118     // Determine visual format by parsing the UI marker in the context.
1119     static Kuit::VisualFormat formatFromUiMarker(const QString &context, const KuitSetup &setup);
1120 
1121     // Determine if text has block structure (multiple paragraphs, etc).
1122     static bool determineIsStructured(const QString &text, const KuitSetup &setup);
1123 
1124     // Format KUIT text into visual text.
1125     QString toVisualText(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const;
1126 
1127     // Final touches to the formatted text.
1128     QString finalizeVisualText(const QString &ftext, Kuit::VisualFormat format) const;
1129 
1130     // In case of markup errors, try to make result not look too bad.
1131     QString salvageMarkup(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const;
1132 
1133     // Data for XML parsing state.
1134     class OpenEl
1135     {
1136     public:
1137         enum Handling { Proper, Ignored, Dropout };
1138 
1139         KuitTag tag;
1140         QString name;
1141         QHash<QString, QString> attributes;
1142         QString attribStr;
1143         Handling handling;
1144         QString formattedText;
1145         QStringList tagPath;
1146     };
1147 
1148     // Gather data about current element for the parse state.
1149     KuitFormatterPrivate::OpenEl parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const;
1150 
1151     // Format text of the element.
1152     QString formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const;
1153 
1154     // Count number of newlines at start and at end of text.
1155     static void countWrappingNewlines(const QString &ptext, int &numle, int &numtr);
1156 
1157 private:
1158     QString language;
1159     QStringList languageAsList;
1160 
1161     QHash<Kuit::VisualFormat, QString> comboKeyDelim;
1162     QHash<Kuit::VisualFormat, QString> guiPathDelim;
1163 
1164     QHash<QString, QString> keyNames;
1165 };
1166 
KuitFormatterPrivate(const QString & language_)1167 KuitFormatterPrivate::KuitFormatterPrivate(const QString &language_)
1168     : language(language_)
1169 {
1170 }
1171 
format(const QByteArray & domain,const QString & context,const QString & text,Kuit::VisualFormat format) const1172 QString KuitFormatterPrivate::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const
1173 {
1174     const KuitSetup &setup = Kuit::setupForDomain(domain);
1175 
1176     // If format is undefined, determine it based on UI marker inside context.
1177     Kuit::VisualFormat resolvedFormat = format;
1178     if (resolvedFormat == Kuit::UndefinedFormat) {
1179         resolvedFormat = formatFromUiMarker(context, setup);
1180     }
1181 
1182     // Quick check: are there any tags at all?
1183     QString ftext;
1184     if (text.indexOf(QL1C('<')) < 0) {
1185         ftext = finalizeVisualText(text, resolvedFormat);
1186     } else {
1187         // Format the text.
1188         ftext = toVisualText(text, resolvedFormat, setup);
1189         if (ftext.isEmpty()) { // error while processing markup
1190             ftext = salvageMarkup(text, resolvedFormat, setup);
1191         }
1192     }
1193     return ftext;
1194 }
1195 
formatFromUiMarker(const QString & context,const KuitSetup & setup)1196 Kuit::VisualFormat KuitFormatterPrivate::formatFromUiMarker(const QString &context, const KuitSetup &setup)
1197 {
1198     KuitStaticData *s = staticData();
1199 
1200     QString roleName;
1201     QString cueName;
1202     QString formatName;
1203     parseUiMarker(context, roleName, cueName, formatName);
1204 
1205     // Set role from name.
1206     Kuit::Role role = s->rolesByName.value(roleName, Kuit::UndefinedRole);
1207     if (role == Kuit::UndefinedRole) { // unknown role
1208         if (!roleName.isEmpty()) {
1209             qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker in context {%2}.").arg(roleName, shorten(context));
1210         }
1211     }
1212 
1213     // Set subcue from name.
1214     Kuit::Cue cue;
1215     if (role != Kuit::UndefinedRole) {
1216         cue = s->cuesByName.value(cueName, Kuit::UndefinedCue);
1217         if (cue != Kuit::UndefinedCue) { // known subcue
1218             if (!s->knownRoleCues.value(role).contains(cue)) {
1219                 cue = Kuit::UndefinedCue;
1220                 qCWarning(KI18N_KUIT)
1221                     << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker in context {%3}.").arg(cueName, roleName, shorten(context));
1222             }
1223         } else { // unknown or not given subcue
1224             if (!cueName.isEmpty()) {
1225                 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker in context {%2}.").arg(cueName, shorten(context));
1226             }
1227         }
1228     } else {
1229         // Bad role, silently ignore the cue.
1230         cue = Kuit::UndefinedCue;
1231     }
1232 
1233     // Set format from name, or by derivation from context/subcue.
1234     Kuit::VisualFormat format = s->formatsByName.value(formatName, Kuit::UndefinedFormat);
1235     if (format == Kuit::UndefinedFormat) { // unknown or not given format
1236         // Check first if there is a format defined for role/subcue
1237         // combination, then for role only, otherwise default to undefined.
1238         if (setup.d->formatsByRoleCue.contains(role)) {
1239             if (setup.d->formatsByRoleCue.value(role).contains(cue)) {
1240                 format = setup.d->formatsByRoleCue.value(role).value(cue);
1241             } else {
1242                 format = setup.d->formatsByRoleCue.value(role).value(Kuit::UndefinedCue);
1243             }
1244         }
1245         if (!formatName.isEmpty()) {
1246             qCWarning(KI18N_KUIT) << QStringLiteral("Unknown format '/%1' in UI marker for message {%2}.").arg(formatName, shorten(context));
1247         }
1248     }
1249     if (format == Kuit::UndefinedFormat) {
1250         format = Kuit::PlainText;
1251     }
1252 
1253     return format;
1254 }
1255 
determineIsStructured(const QString & text,const KuitSetup & setup)1256 bool KuitFormatterPrivate::determineIsStructured(const QString &text, const KuitSetup &setup)
1257 {
1258     // If the text opens with a structuring tag, then it is structured,
1259     // otherwise not. Leading whitespace is ignored for this purpose.
1260     static const QRegularExpression opensWithTagRx(QStringLiteral("^\\s*<\\s*(\\w+)[^>]*>"));
1261     bool isStructured = false;
1262     const QRegularExpressionMatch match = opensWithTagRx.match(text);
1263     if (match.hasMatch()) {
1264         const QString tagName = match.captured(1).toLower();
1265         if (setup.d->knownTags.contains(tagName)) {
1266             const KuitTag &tag = setup.d->knownTags.value(tagName);
1267             isStructured = (tag.type == Kuit::StructTag);
1268         }
1269     }
1270     return isStructured;
1271 }
1272 
1273 static const char s_entitySubRx[] = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+";
1274 
toVisualText(const QString & text_,Kuit::VisualFormat format,const KuitSetup & setup) const1275 QString KuitFormatterPrivate::toVisualText(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const
1276 {
1277     KuitStaticData *s = staticData();
1278 
1279     // Replace &-shortcut marker with "&amp;", not to confuse the parser;
1280     // but do not touch & which forms an XML entity as it is.
1281     QString original = text_;
1282     // Regex is (see s_entitySubRx var): ^([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);
1283     static const QRegularExpression restRx(QLatin1String("^(") + QLatin1String(s_entitySubRx) + QLatin1String(");"));
1284 
1285     QString text;
1286     int p = original.indexOf(QL1C('&'));
1287     while (p >= 0) {
1288         text.append(QStringView(original).mid(0, p + 1));
1289         original.remove(0, p + 1);
1290         if (original.indexOf(restRx) != 0) { // not an entity
1291             text.append(QSL("amp;"));
1292         }
1293         p = original.indexOf(QL1C('&'));
1294     }
1295     text.append(original);
1296 
1297     // FIXME: Do this and then check proper use of structuring and phrase tags.
1298 #if 0
1299     // Determine whether this is block-structured text.
1300     bool isStructured = determineIsStructured(text, setup);
1301 #endif
1302 
1303     const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__");
1304     // Add top tag, not to confuse the parser.
1305     text = QStringLiteral("<%2>%1</%2>").arg(text, INTERNAL_TOP_TAG_NAME);
1306 
1307     QStack<OpenEl> openEls;
1308     QXmlStreamReader xml(text);
1309     xml.setEntityResolver(&s->xmlEntityResolver);
1310     QStringView lastElementName;
1311 
1312     while (!xml.atEnd()) {
1313         xml.readNext();
1314 
1315         if (xml.isStartElement()) {
1316             lastElementName = xml.name();
1317 
1318             // Find first proper enclosing element.
1319             OpenEl enclosingOel;
1320             for (int i = openEls.size() - 1; i >= 0; --i) {
1321                 if (openEls[i].handling == OpenEl::Proper) {
1322                     enclosingOel = openEls[i];
1323                     break;
1324                 }
1325             }
1326 
1327             // Collect data about this element.
1328             OpenEl oel = parseOpenEl(xml, enclosingOel, text, setup);
1329 
1330             // Record the new element on the parse stack.
1331             openEls.push(oel);
1332         } else if (xml.isEndElement()) {
1333             // Get closed element data.
1334             OpenEl oel = openEls.pop();
1335 
1336             // If this was closing of the top element, we're done.
1337             if (openEls.isEmpty()) {
1338                 // Return with final touches applied.
1339                 return finalizeVisualText(oel.formattedText, format);
1340             }
1341 
1342             // Append formatted text segment.
1343             QString ptext = openEls.top().formattedText; // preceding text
1344             openEls.top().formattedText += formatSubText(ptext, oel, format, setup);
1345         } else if (xml.isCharacters()) {
1346             // Stream reader will automatically resolve default XML entities,
1347             // which is not desired in this case, as the entities are to be
1348             // resolved in finalizeVisualText. Convert back into entities.
1349             const QString ctext = xml.text().toString();
1350             QString nctext;
1351             for (const QChar c : ctext) {
1352                 if (s->xmlEntitiesInverse.contains(c)) {
1353                     const QString entName = s->xmlEntitiesInverse[c];
1354                     nctext += QL1C('&') + entName + QL1C(';');
1355                 } else {
1356                     nctext += c;
1357                 }
1358             }
1359             openEls.top().formattedText += nctext;
1360         }
1361     }
1362 
1363     if (xml.hasError()) {
1364         qCWarning(KI18N_KUIT) << QStringLiteral("Markup error in message {%1}: %2. Last tag parsed: %3. Complete message follows:\n%4")
1365                                      .arg(shorten(text), xml.errorString(), lastElementName.toString(), text);
1366         return QString();
1367     }
1368 
1369     // Cannot reach here.
1370     return text;
1371 }
1372 
1373 KuitFormatterPrivate::OpenEl
parseOpenEl(const QXmlStreamReader & xml,const OpenEl & enclosingOel,const QString & text,const KuitSetup & setup) const1374 KuitFormatterPrivate::parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const
1375 {
1376     OpenEl oel;
1377     oel.name = xml.name().toString().toLower();
1378 
1379     // Collect attribute names and values, and format attribute string.
1380     QStringList attribNames;
1381     QStringList attribValues;
1382     const auto listAttributes = xml.attributes();
1383     attribNames.reserve(listAttributes.size());
1384     attribValues.reserve(listAttributes.size());
1385     for (const QXmlStreamAttribute &xatt : listAttributes) {
1386         attribNames += xatt.name().toString().toLower();
1387         attribValues += xatt.value().toString();
1388         QChar qc = attribValues.last().indexOf(QL1C('\'')) < 0 ? QL1C('\'') : QL1C('"');
1389         oel.attribStr += QL1C(' ') + attribNames.last() + QL1C('=') + qc + attribValues.last() + qc;
1390     }
1391 
1392     if (setup.d->knownTags.contains(oel.name)) { // known KUIT element
1393         const KuitTag &tag = setup.d->knownTags.value(oel.name);
1394         const KuitTag &etag = setup.d->knownTags.value(enclosingOel.name);
1395 
1396         // If this element can be contained within enclosing element,
1397         // mark it proper, otherwise mark it for removal.
1398         if (tag.name.isEmpty() || tag.type == Kuit::PhraseTag || etag.type == Kuit::StructTag) {
1399             oel.handling = OpenEl::Proper;
1400         } else {
1401             oel.handling = OpenEl::Dropout;
1402             qCWarning(KI18N_KUIT)
1403                 << QStringLiteral("Structuring tag ('%1') cannot be subtag of phrase tag ('%2') in message {%3}.").arg(tag.name, etag.name, shorten(text));
1404         }
1405 
1406         // Resolve attributes and compute attribute set key.
1407         QSet<QString> attset;
1408         for (int i = 0; i < attribNames.size(); ++i) {
1409             QString att = attribNames[i];
1410             if (tag.knownAttribs.contains(att)) {
1411                 attset << att;
1412                 oel.attributes[att] = attribValues[i];
1413             } else {
1414                 qCWarning(KI18N_KUIT) << QStringLiteral("Attribute '%1' not defined for tag '%2' in message {%3}.").arg(att, tag.name, shorten(text));
1415             }
1416         }
1417 
1418         // Continue tag path.
1419         oel.tagPath = enclosingOel.tagPath;
1420         oel.tagPath.prepend(enclosingOel.name);
1421 
1422     } else { // unknown element, leave it in verbatim
1423         oel.handling = OpenEl::Ignored;
1424         qCWarning(KI18N_KUIT) << QStringLiteral("Tag '%1' is not defined in message {%2}.").arg(oel.name, shorten(text));
1425     }
1426 
1427     return oel;
1428 }
1429 
formatSubText(const QString & ptext,const OpenEl & oel,Kuit::VisualFormat format,const KuitSetup & setup) const1430 QString KuitFormatterPrivate::formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const
1431 {
1432     if (oel.handling == OpenEl::Proper) {
1433         const KuitTag &tag = setup.d->knownTags.value(oel.name);
1434         QString ftext = tag.format(languageAsList, oel.attributes, oel.formattedText, oel.tagPath, format);
1435 
1436         // Handle leading newlines, if this is not start of the text
1437         // (ptext is the preceding text).
1438         if (!ptext.isEmpty() && tag.leadingNewlines > 0) {
1439             // Count number of present newlines.
1440             int pnumle;
1441             int pnumtr;
1442             int fnumle;
1443             int fnumtr;
1444             countWrappingNewlines(ptext, pnumle, pnumtr);
1445             countWrappingNewlines(ftext, fnumle, fnumtr);
1446             // Number of leading newlines already present.
1447             int numle = pnumtr + fnumle;
1448             // The required extra newlines.
1449             QString strle;
1450             if (numle < tag.leadingNewlines) {
1451                 strle = QString(tag.leadingNewlines - numle, QL1C('\n'));
1452             }
1453             ftext = strle + ftext;
1454         }
1455 
1456         return ftext;
1457 
1458     } else if (oel.handling == OpenEl::Ignored) {
1459         return QL1C('<') + oel.name + oel.attribStr + QL1C('>') + oel.formattedText + QSL("</") + oel.name + QL1C('>');
1460 
1461     } else { // oel.handling == OpenEl::Dropout
1462         return oel.formattedText;
1463     }
1464 }
1465 
countWrappingNewlines(const QString & text,int & numle,int & numtr)1466 void KuitFormatterPrivate::countWrappingNewlines(const QString &text, int &numle, int &numtr)
1467 {
1468     int len = text.length();
1469     // Number of newlines at start of text.
1470     numle = 0;
1471     while (numle < len && text[numle] == QL1C('\n')) {
1472         ++numle;
1473     }
1474     // Number of newlines at end of text.
1475     numtr = 0;
1476     while (numtr < len && text[len - numtr - 1] == QL1C('\n')) {
1477         ++numtr;
1478     }
1479 }
1480 
finalizeVisualText(const QString & text_,Kuit::VisualFormat format) const1481 QString KuitFormatterPrivate::finalizeVisualText(const QString &text_, Kuit::VisualFormat format) const
1482 {
1483     KuitStaticData *s = staticData();
1484 
1485     QString text = text_;
1486 
1487     // Resolve XML entities.
1488     if (format != Kuit::RichText) {
1489         // regex is (see s_entitySubRx var): (&([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);)
1490         static const QRegularExpression entRx(QLatin1String("(&(") + QLatin1String(s_entitySubRx) + QLatin1String(");)"));
1491         QRegularExpressionMatch match;
1492         QString plain;
1493         while ((match = entRx.match(text)).hasMatch()) {
1494             plain.append(QStringView(text).mid(0, match.capturedStart(0)));
1495             text.remove(0, match.capturedEnd(0));
1496             const QString ent = match.captured(2);
1497             if (ent.startsWith(QL1C('#'))) { // numeric character entity
1498                 bool ok;
1499 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
1500                 QStringView entView(ent);
1501                 const QChar c = ent.at(1) == QL1C('x') ? QChar(entView.mid(2).toInt(&ok, 16)) : QChar(entView.mid(1).toInt(&ok, 10));
1502 #else
1503                 const QChar c = ent.at(1) == QL1C('x') ? QChar(ent.midRef(2).toInt(&ok, 16)) : QChar(ent.midRef(1).toInt(&ok, 10));
1504 #endif
1505                 if (ok) {
1506                     plain.append(c);
1507                 } else { // unknown Unicode point, leave as is
1508                     plain.append(match.capturedView(0));
1509                 }
1510             } else if (s->xmlEntities.contains(ent)) { // known entity
1511                 plain.append(s->xmlEntities[ent]);
1512             } else { // unknown entity, just leave as is
1513                 plain.append(match.capturedView(0));
1514             }
1515         }
1516         plain.append(text);
1517         text = plain;
1518     }
1519 
1520     // Add top tag.
1521     if (format == Kuit::RichText) {
1522         text = QLatin1String("<html>") + text + QLatin1String("</html>");
1523     }
1524 
1525     return text;
1526 }
1527 
salvageMarkup(const QString & text_,Kuit::VisualFormat format,const KuitSetup & setup) const1528 QString KuitFormatterPrivate::salvageMarkup(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const
1529 {
1530     QString text = text_;
1531     QString ntext;
1532 
1533     // Resolve tags simple-mindedly.
1534 
1535     // - tags with content
1536     static const QRegularExpression wrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)"), QRegularExpression::InvertedGreedinessOption);
1537     QRegularExpressionMatchIterator iter = wrapRx.globalMatch(text);
1538     QRegularExpressionMatch match;
1539     int pos = 0;
1540     while (iter.hasNext()) {
1541         match = iter.next();
1542         ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos);
1543         const QString tagname = match.captured(2).toLower();
1544         const QString content = salvageMarkup(match.captured(4), format, setup);
1545         if (setup.d->knownTags.contains(tagname)) {
1546             const KuitTag &tag = setup.d->knownTags.value(tagname);
1547             QHash<QString, QString> attributes;
1548             // TODO: Do not ignore attributes (in match.captured(3)).
1549             ntext += tag.format(languageAsList, attributes, content, QStringList(), format);
1550         } else {
1551             ntext += match.captured(1) + content + match.captured(5);
1552         }
1553         pos = match.capturedEnd(0);
1554     }
1555     // get the remaining part after the last match in "text"
1556     ntext += QStringView(text).mid(pos);
1557     text = ntext;
1558 
1559     // - tags without content
1560     static const QRegularExpression nowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>"), QRegularExpression::InvertedGreedinessOption);
1561     iter = nowrRx.globalMatch(text);
1562     pos = 0;
1563     ntext.clear();
1564     while (iter.hasNext()) {
1565         match = iter.next();
1566         ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos);
1567         const QString tagname = match.captured(1).toLower();
1568         if (setup.d->knownTags.contains(tagname)) {
1569             const KuitTag &tag = setup.d->knownTags.value(tagname);
1570             ntext += tag.format(languageAsList, QHash<QString, QString>(), QString(), QStringList(), format);
1571         } else {
1572             ntext += match.captured(0);
1573         }
1574         pos = match.capturedEnd(0);
1575     }
1576     // get the remaining part after the last match in "text"
1577     ntext += QStringView(text).mid(pos);
1578     text = ntext;
1579 
1580     // Add top tag.
1581     if (format == Kuit::RichText) {
1582         text = QStringLiteral("<html>") + text + QStringLiteral("</html>");
1583     }
1584 
1585     return text;
1586 }
1587 
KuitFormatter(const QString & language)1588 KuitFormatter::KuitFormatter(const QString &language)
1589     : d(new KuitFormatterPrivate(language))
1590 {
1591 }
1592 
~KuitFormatter()1593 KuitFormatter::~KuitFormatter()
1594 {
1595     delete d;
1596 }
1597 
format(const QByteArray & domain,const QString & context,const QString & text,Kuit::VisualFormat format) const1598 QString KuitFormatter::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const
1599 {
1600     return d->format(domain, context, text, format);
1601 }
1602