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

source code of ki18n/src/i18n/kuitsetup.cpp