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 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(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 << 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
394 const QRegularExpressionMatch match = delimRx.match(subject: shstr);
395 QStringList keys;
396 if (match.hasMatch()) { // delimiter found, multi-key shortcut
397 const QString oldDelim = match.captured(nth: 0);
398 keys = shstr.split(sep: oldDelim, behavior: Qt::SkipEmptyParts);
399 } else { // single-key shortcut, no delimiter found
400 keys.append(t: shstr);
401 }
402
403 for (QString &key : keys) {
404 // Normalize key
405 key = key.trimmed();
406 auto nameIt = keyNames.constFind(key: key.toLower());
407 if (nameIt != keyNames.constEnd()) {
408 key = nameIt->toString(languages);
409 }
410 }
411 const QString delim = comboKeyDelim.value(key: format).toString(languages);
412 return keys.join(sep: delim);
413}
414
415QString KuitStaticData::toInterfacePath(const QStringList &languages, const QString &inpstr, Kuit::VisualFormat format)
416{
417 // Take '/', '|' or "->" as input path delimiter,
418 // whichever is first encountered.
419 static const QRegularExpression delimRx(QStringLiteral("\\||->"));
420 const QRegularExpressionMatch match = delimRx.match(subject: inpstr);
421 if (match.hasMatch()) { // multi-element path
422 const QString oldDelim = match.captured(nth: 0);
423 QStringList guiels = inpstr.split(sep: oldDelim, behavior: Qt::SkipEmptyParts);
424 const QString delim = guiPathDelim.value(key: format).toString(languages);
425 return guiels.join(sep: delim);
426 }
427
428 // single-element path, no delimiter found
429 return inpstr;
430}
431
432Q_GLOBAL_STATIC(KuitStaticData, staticData)
433
434static QString attributeSetKey(const QStringList &attribNames_)
435{
436 QStringList attribNames = attribNames_;
437 std::sort(first: attribNames.begin(), last: attribNames.end());
438 QString key = QL1C('[') + attribNames.join(QL1C(' ')) + QL1C(']');
439 return key;
440}
441
442class KuitTag
443{
444public:
445 QString name;
446 Kuit::TagClass type;
447 QSet<QString> knownAttribs;
448 QHash<QString, QHash<Kuit::VisualFormat, QStringList>> attributeOrders;
449 QHash<QString, QHash<Kuit::VisualFormat, KLocalizedString>> patterns;
450 QHash<QString, QHash<Kuit::VisualFormat, Kuit::TagFormatter>> formatters;
451 int leadingNewlines;
452
453 KuitTag(const QString &_name, Kuit::TagClass _type)
454 : name(_name)
455 , type(_type)
456 {
457 }
458 KuitTag() = default;
459
460 QString format(const QStringList &languages,
461 const QHash<QString, QString> &attributes,
462 const QString &text,
463 const QStringList &tagPath,
464 Kuit::VisualFormat format) const;
465};
466
467QString KuitTag::format(const QStringList &languages,
468 const QHash<QString, QString> &attributes,
469 const QString &text,
470 const QStringList &tagPath,
471 Kuit::VisualFormat format) const
472{
473 KuitStaticData *s = staticData();
474 QString formattedText = text;
475 QString attribKey = attributeSetKey(attribNames_: attributes.keys());
476 const QHash<Kuit::VisualFormat, KLocalizedString> pattern = patterns.value(key: attribKey);
477 auto patternIt = pattern.constFind(key: format);
478 if (patternIt != pattern.constEnd()) {
479 QString modText;
480 Kuit::TagFormatter formatter = formatters.value(key: attribKey).value(key: format);
481 if (formatter != nullptr) {
482 modText = formatter(languages, name, attributes, text, tagPath, format);
483 } else {
484 modText = text;
485 }
486 KLocalizedString aggText = *patternIt;
487 // line below is first-aid fix.for e.g. <emphasis strong='true'>.
488 // TODO: proper handling of boolean attributes still needed
489 aggText = aggText.relaxSubs();
490 if (!aggText.isEmpty()) {
491 aggText = aggText.subs(a: modText);
492 const QStringList attributeOrder = attributeOrders.value(key: attribKey).value(key: format);
493 for (const QString &attribName : attributeOrder) {
494 aggText = aggText.subs(a: attributes.value(key: attribName));
495 }
496 formattedText = aggText.ignoreMarkup().toString(languages);
497 } else {
498 formattedText = modText;
499 }
500 } else if (patterns.contains(key: attribKey)) {
501 qCWarning(KI18N_KUIT)
502 << QStringLiteral("Undefined visual format for tag <%1> and attribute combination %2: %3.").arg(args: name, args&: attribKey, args: s->namesByFormat.value(key: format));
503 } else {
504 qCWarning(KI18N_KUIT) << QStringLiteral("Undefined attribute combination for tag <%1>: %2.").arg(args: name, args&: attribKey);
505 }
506 return formattedText;
507}
508
509KuitSetup &Kuit::setupForDomain(const QByteArray &domain)
510{
511 KuitStaticData *s = staticData();
512 KuitSetup *setup = s->domainSetups.value(key: domain);
513 if (!setup) {
514 setup = new KuitSetup(domain);
515 s->domainSetups.insert(key: domain, value: setup);
516 }
517 return *setup;
518}
519
520class KuitSetupPrivate
521{
522public:
523 void setTagPattern(const QString &tagName,
524 const QStringList &attribNames,
525 Kuit::VisualFormat format,
526 const KLocalizedString &pattern,
527 Kuit::TagFormatter formatter,
528 int leadingNewlines);
529
530 void setTagClass(const QString &tagName, Kuit::TagClass aClass);
531
532 void setFormatForMarker(const QString &marker, Kuit::VisualFormat format);
533
534 void setDefaultMarkup();
535 void setDefaultFormats();
536
537 QByteArray domain;
538 QHash<QString, KuitTag> knownTags;
539 QHash<Kuit::Role, QHash<Kuit::Cue, Kuit::VisualFormat>> formatsByRoleCue;
540};
541
542void KuitSetupPrivate::setTagPattern(const QString &tagName,
543 const QStringList &attribNames_,
544 Kuit::VisualFormat format,
545 const KLocalizedString &pattern,
546 Kuit::TagFormatter formatter,
547 int leadingNewlines_)
548{
549 auto tagIt = knownTags.find(key: tagName);
550 if (tagIt == knownTags.end()) {
551 tagIt = knownTags.insert(key: tagName, value: KuitTag(tagName, Kuit::PhraseTag));
552 }
553
554 KuitTag &tag = *tagIt;
555
556 QStringList attribNames = attribNames_;
557 attribNames.removeAll(t: QString());
558 for (const QString &attribName : std::as_const(t&: attribNames)) {
559 tag.knownAttribs.insert(value: attribName);
560 }
561 QString attribKey = attributeSetKey(attribNames_: attribNames);
562 tag.attributeOrders[attribKey][format] = attribNames;
563 tag.patterns[attribKey][format] = pattern;
564 tag.formatters[attribKey][format] = formatter;
565 tag.leadingNewlines = leadingNewlines_;
566}
567
568void KuitSetupPrivate::setTagClass(const QString &tagName, Kuit::TagClass aClass)
569{
570 auto tagIt = knownTags.find(key: tagName);
571 if (tagIt == knownTags.end()) {
572 knownTags.insert(key: tagName, value: KuitTag(tagName, aClass));
573 } else {
574 tagIt->type = aClass;
575 }
576}
577
578void KuitSetupPrivate::setFormatForMarker(const QString &marker, Kuit::VisualFormat format)
579{
580 KuitStaticData *s = staticData();
581
582 QString roleName;
583 QString cueName;
584 QString formatName;
585 parseUiMarker(context_: marker, roleName, cueName, formatName);
586
587 Kuit::Role role;
588 auto roleIt = s->rolesByName.constFind(key: roleName);
589 if (roleIt != s->rolesByName.constEnd()) {
590 role = *roleIt;
591 } else if (!roleName.isEmpty()) {
592 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker {%2}, visual format not set.").arg(args&: roleName, args: marker);
593 return;
594 } else {
595 qCWarning(KI18N_KUIT) << QStringLiteral("Empty role in UI marker {%1}, visual format not set.").arg(a: marker);
596 return;
597 }
598
599 Kuit::Cue cue;
600 auto cueIt = s->cuesByName.constFind(key: cueName);
601 if (cueIt != s->cuesByName.constEnd()) {
602 cue = *cueIt;
603 if (!s->knownRoleCues.value(key: role).contains(value: cue)) {
604 qCWarning(KI18N_KUIT)
605 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker {%3}, visual format not set.").arg(args&: cueName, args&: roleName, args: marker);
606 return;
607 }
608 } else if (!cueName.isEmpty()) {
609 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker {%2}, visual format not set.").arg(args&: cueName, args: marker);
610 return;
611 } else {
612 cue = Kuit::UndefinedCue;
613 }
614
615 formatsByRoleCue[role][cue] = format;
616}
617
618#define TAG_FORMATTER_ARGS \
619 const QStringList &languages, const QString &tagName, const QHash<QString, QString> &attributes, const QString &text, const QStringList &tagPath, \
620 Kuit::VisualFormat format
621
622static QString tagFormatterFilename(TAG_FORMATTER_ARGS)
623{
624 Q_UNUSED(languages);
625 Q_UNUSED(tagName);
626 Q_UNUSED(attributes);
627 Q_UNUSED(tagPath);
628#ifdef Q_OS_WIN
629 // with rich text the path can include <foo>...</foo> which will be replaced by <foo>...<\foo> on Windows!
630 // the same problem also happens for tags such as <br/> -> <br\>
631 if (format == Kuit::RichText) {
632 // replace all occurrences of "</" or "/>" to make sure toNativeSeparators() doesn't destroy XML markup
633 const auto KUIT_CLOSE_XML_REPLACEMENT = QStringLiteral("__kuit_close_xml_tag__");
634 const auto KUIT_NOTEXT_XML_REPLACEMENT = QStringLiteral("__kuit_notext_xml_tag__");
635
636 QString result = text;
637 result.replace(QStringLiteral("</"), KUIT_CLOSE_XML_REPLACEMENT);
638 result.replace(QStringLiteral("/>"), KUIT_NOTEXT_XML_REPLACEMENT);
639 result = QDir::toNativeSeparators(result);
640 result.replace(KUIT_CLOSE_XML_REPLACEMENT, QStringLiteral("</"));
641 result.replace(KUIT_NOTEXT_XML_REPLACEMENT, QStringLiteral("/>"));
642 return result;
643 }
644#else
645 Q_UNUSED(format);
646#endif
647 return QDir::toNativeSeparators(pathName: text);
648}
649
650static QString tagFormatterShortcut(TAG_FORMATTER_ARGS)
651{
652 Q_UNUSED(tagName);
653 Q_UNUSED(attributes);
654 Q_UNUSED(tagPath);
655 KuitStaticData *s = staticData();
656 return s->toKeyCombo(languages, shstr: text, format);
657}
658
659static QString tagFormatterInterface(TAG_FORMATTER_ARGS)
660{
661 Q_UNUSED(tagName);
662 Q_UNUSED(attributes);
663 Q_UNUSED(tagPath);
664 KuitStaticData *s = staticData();
665 return s->toInterfacePath(languages, inpstr: text, format);
666}
667
668void KuitSetupPrivate::setDefaultMarkup()
669{
670 using namespace Kuit;
671
672 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__");
673 const QString TITLE = QStringLiteral("title");
674 const QString EMPHASIS = QStringLiteral("emphasis");
675 const QString COMMAND = QStringLiteral("command");
676 const QString WARNING = QStringLiteral("warning");
677 const QString LINK = QStringLiteral("link");
678 const QString NOTE = QStringLiteral("note");
679
680 // clang-format off
681 // Macro to hide message from extraction.
682#define HI18NC ki18nc
683
684 // Macro to expedite setting the patterns.
685#undef SET_PATTERN
686#define SET_PATTERN(tagName, attribNames_, format, pattern, formatter, leadNl) \
687 do { \
688 QStringList attribNames; \
689 attribNames << attribNames_; \
690 setTagPattern(tagName, attribNames, format, pattern, formatter, leadNl); \
691 /* Make TermText pattern same as PlainText if not explicitly given. */ \
692 KuitTag &tag = knownTags[tagName]; \
693 QString attribKey = attributeSetKey(attribNames); \
694 if (format == PlainText && !tag.patterns[attribKey].contains(TermText)) { \
695 setTagPattern(tagName, attribNames, TermText, pattern, formatter, leadNl); \
696 } \
697 } while (0)
698
699 // NOTE: The following "i18n:" comments are oddly placed in order that
700 // xgettext extracts them properly.
701
702 // -------> Internal top tag
703 setTagClass(tagName: INTERNAL_TOP_TAG_NAME, aClass: StructTag);
704 SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), PlainText,
705 HI18NC("tag-format-pattern <> plain",
706 // i18n: KUIT pattern, see the comment to the first of these entries above.
707 "%1"),
708 nullptr, 0);
709 SET_PATTERN(INTERNAL_TOP_TAG_NAME, QString(), RichText,
710 HI18NC("tag-format-pattern <> rich",
711 // i18n: KUIT pattern, see the comment to the first of these entries above.
712 "%1"),
713 nullptr, 0);
714
715 // -------> Title
716 setTagClass(tagName: TITLE, aClass: StructTag);
717 SET_PATTERN(TITLE, QString(), PlainText,
718 ki18nc("tag-format-pattern <title> plain",
719 // i18n: The messages with context "tag-format-pattern <tag ...> format"
720 // are KUIT patterns for formatting the text found inside KUIT tags.
721 // The format is either "plain" or "rich", and tells if the pattern
722 // is used for plain text or rich text (which can use HTML tags).
723 // You may be in general satisfied with the patterns as they are in the
724 // original. Some things you may consider changing:
725 // - the proper quotes, those used in msgid are English-standard
726 // - the <i> and <b> tags, does your language script work well with them?
727 "== %1 =="),
728 nullptr, 2);
729 SET_PATTERN(TITLE, QString(), RichText,
730 ki18nc("tag-format-pattern <title> rich",
731 // i18n: KUIT pattern, see the comment to the first of these entries above.
732 "<h2>%1</h2>"),
733 nullptr, 2);
734
735 // -------> Subtitle
736 setTagClass(QSL("subtitle"), aClass: StructTag);
737 SET_PATTERN(QSL("subtitle"), QString(), PlainText,
738 ki18nc("tag-format-pattern <subtitle> plain",
739 // i18n: KUIT pattern, see the comment to the first of these entries above.
740 "~ %1 ~"),
741 nullptr, 2);
742 SET_PATTERN(QSL("subtitle"), QString(), RichText,
743 ki18nc("tag-format-pattern <subtitle> rich",
744 // i18n: KUIT pattern, see the comment to the first of these entries above.
745 "<h3>%1</h3>"),
746 nullptr, 2);
747
748 // -------> Para
749 setTagClass(QSL("para"), aClass: StructTag);
750 SET_PATTERN(QSL("para"), QString(), PlainText,
751 ki18nc("tag-format-pattern <para> plain",
752 // i18n: KUIT pattern, see the comment to the first of these entries above.
753 "%1"),
754 nullptr, 2);
755 SET_PATTERN(QSL("para"), QString(), RichText,
756 ki18nc("tag-format-pattern <para> rich",
757 // i18n: KUIT pattern, see the comment to the first of these entries above.
758 "<p>%1</p>"),
759 nullptr, 2);
760
761 // -------> List
762 setTagClass(QSL("list"), aClass: StructTag);
763 SET_PATTERN(QSL("list"), QString(), PlainText,
764 ki18nc("tag-format-pattern <list> plain",
765 // i18n: KUIT pattern, see the comment to the first of these entries above.
766 "%1"),
767 nullptr, 1);
768 SET_PATTERN(QSL("list"), QString(), RichText,
769 ki18nc("tag-format-pattern <list> rich",
770 // i18n: KUIT pattern, see the comment to the first of these entries above.
771 "<ul>%1</ul>"),
772 nullptr, 1);
773
774 // -------> Item
775 setTagClass(QSL("item"), aClass: StructTag);
776 SET_PATTERN(QSL("item"), QString(), PlainText,
777 ki18nc("tag-format-pattern <item> plain",
778 // i18n: KUIT pattern, see the comment to the first of these entries above.
779 " * %1"),
780 nullptr, 1);
781 SET_PATTERN(QSL("item"), QString(), RichText,
782 ki18nc("tag-format-pattern <item> rich",
783 // i18n: KUIT pattern, see the comment to the first of these entries above.
784 "<li>%1</li>"),
785 nullptr, 1);
786
787 // -------> Note
788 SET_PATTERN(NOTE, QString(), PlainText,
789 ki18nc("tag-format-pattern <note> plain",
790 // i18n: KUIT pattern, see the comment to the first of these entries above.
791 "Note: %1"),
792 nullptr, 0);
793 SET_PATTERN(NOTE, QString(), RichText,
794 ki18nc("tag-format-pattern <note> rich",
795 // i18n: KUIT pattern, see the comment to the first of these entries above.
796 "<i>Note</i>: %1"),
797 nullptr, 0);
798 SET_PATTERN(NOTE, QSL("label"), PlainText,
799 ki18nc("tag-format-pattern <note label=> plain\n"
800 "%1 is the text, %2 is the note label",
801 // i18n: KUIT pattern, see the comment to the first of these entries above.
802 "%2: %1"),
803 nullptr, 0);
804 SET_PATTERN(NOTE, QSL("label"), RichText,
805 ki18nc("tag-format-pattern <note label=> rich\n"
806 "%1 is the text, %2 is the note label",
807 // i18n: KUIT pattern, see the comment to the first of these entries above.
808 "<i>%2</i>: %1"),
809 nullptr, 0);
810
811 // -------> Warning
812 SET_PATTERN(WARNING, QString(), PlainText,
813 ki18nc("tag-format-pattern <warning> plain",
814 // i18n: KUIT pattern, see the comment to the first of these entries above.
815 "WARNING: %1"),
816 nullptr, 0);
817 SET_PATTERN(WARNING, QString(), RichText,
818 ki18nc("tag-format-pattern <warning> rich",
819 // i18n: KUIT pattern, see the comment to the first of these entries above.
820 "<b>Warning</b>: %1"),
821 nullptr, 0);
822 SET_PATTERN(WARNING, QSL("label"), PlainText,
823 ki18nc("tag-format-pattern <warning label=> plain\n"
824 "%1 is the text, %2 is the warning label",
825 // i18n: KUIT pattern, see the comment to the first of these entries above.
826 "%2: %1"),
827 nullptr, 0);
828 SET_PATTERN(WARNING, QSL("label"), RichText,
829 ki18nc("tag-format-pattern <warning label=> rich\n"
830 "%1 is the text, %2 is the warning label",
831 // i18n: KUIT pattern, see the comment to the first of these entries above.
832 "<b>%2</b>: %1"),
833 nullptr, 0);
834
835 // -------> Link
836 SET_PATTERN(LINK, QString(), PlainText,
837 ki18nc("tag-format-pattern <link> plain",
838 // i18n: KUIT pattern, see the comment to the first of these entries above.
839 "%1"),
840 nullptr, 0);
841 SET_PATTERN(LINK, QString(), RichText,
842 ki18nc("tag-format-pattern <link> rich",
843 // i18n: KUIT pattern, see the comment to the first of these entries above.
844 "<a href=\"%1\">%1</a>"),
845 nullptr, 0);
846 SET_PATTERN(LINK, QSL("url"), PlainText,
847 ki18nc("tag-format-pattern <link url=> plain\n"
848 "%1 is the descriptive text, %2 is the URL",
849 // i18n: KUIT pattern, see the comment to the first of these entries above.
850 "%1 (%2)"),
851 nullptr, 0);
852 SET_PATTERN(LINK, QSL("url"), RichText,
853 ki18nc("tag-format-pattern <link url=> rich\n"
854 "%1 is the descriptive text, %2 is the URL",
855 // i18n: KUIT pattern, see the comment to the first of these entries above.
856 "<a href=\"%2\">%1</a>"),
857 nullptr, 0);
858
859 // -------> Filename
860 SET_PATTERN(QSL("filename"), QString(), PlainText,
861 ki18nc("tag-format-pattern <filename> plain",
862 // i18n: KUIT pattern, see the comment to the first of these entries above.
863 "‘%1’"),
864 tagFormatterFilename, 0);
865 SET_PATTERN(QSL("filename"), QString(), RichText,
866 ki18nc("tag-format-pattern <filename> rich",
867 // i18n: KUIT pattern, see the comment to the first of these entries above.
868 "‘<tt>%1</tt>’"),
869 tagFormatterFilename, 0);
870
871 // -------> Application
872 SET_PATTERN(QSL("application"), QString(), PlainText,
873 ki18nc("tag-format-pattern <application> plain",
874 // i18n: KUIT pattern, see the comment to the first of these entries above.
875 "%1"),
876 nullptr, 0);
877 SET_PATTERN(QSL("application"), QString(), RichText,
878 ki18nc("tag-format-pattern <application> rich",
879 // i18n: KUIT pattern, see the comment to the first of these entries above.
880 "%1"),
881 nullptr, 0);
882
883 // -------> Command
884 SET_PATTERN(COMMAND, QString(), PlainText,
885 ki18nc("tag-format-pattern <command> plain",
886 // i18n: KUIT pattern, see the comment to the first of these entries above.
887 "%1"),
888 nullptr, 0);
889 SET_PATTERN(COMMAND, QString(), RichText,
890 ki18nc("tag-format-pattern <command> rich",
891 // i18n: KUIT pattern, see the comment to the first of these entries above.
892 "<tt>%1</tt>"),
893 nullptr, 0);
894 SET_PATTERN(COMMAND, QSL("section"), PlainText,
895 ki18nc("tag-format-pattern <command section=> plain\n"
896 "%1 is the command name, %2 is its man section",
897 // i18n: KUIT pattern, see the comment to the first of these entries above.
898 "%1(%2)"),
899 nullptr, 0);
900 SET_PATTERN(COMMAND, QSL("section"), RichText,
901 ki18nc("tag-format-pattern <command section=> rich\n"
902 "%1 is the command name, %2 is its man section",
903 // i18n: KUIT pattern, see the comment to the first of these entries above.
904 "<tt>%1(%2)</tt>"),
905 nullptr, 0);
906
907 // -------> Resource
908 SET_PATTERN(QSL("resource"), QString(), PlainText,
909 ki18nc("tag-format-pattern <resource> plain",
910 // i18n: KUIT pattern, see the comment to the first of these entries above.
911 "“%1”"),
912 nullptr, 0);
913 SET_PATTERN(QSL("resource"), QString(), RichText,
914 ki18nc("tag-format-pattern <resource> rich",
915 // i18n: KUIT pattern, see the comment to the first of these entries above.
916 "“%1”"),
917 nullptr, 0);
918
919 // -------> Icode
920 SET_PATTERN(QSL("icode"), QString(), PlainText,
921 ki18nc("tag-format-pattern <icode> plain",
922 // i18n: KUIT pattern, see the comment to the first of these entries above.
923 "“%1”"),
924 nullptr, 0);
925 SET_PATTERN(QSL("icode"), QString(), RichText,
926 ki18nc("tag-format-pattern <icode> rich",
927 // i18n: KUIT pattern, see the comment to the first of these entries above.
928 "<tt>%1</tt>"),
929 nullptr, 0);
930
931 // -------> Bcode
932 SET_PATTERN(QSL("bcode"), QString(), PlainText,
933 ki18nc("tag-format-pattern <bcode> plain",
934 // i18n: KUIT pattern, see the comment to the first of these entries above.
935 "\n%1\n"),
936 nullptr, 2);
937 SET_PATTERN(QSL("bcode"), QString(), RichText,
938 ki18nc("tag-format-pattern <bcode> rich",
939 // i18n: KUIT pattern, see the comment to the first of these entries above.
940 "<pre>%1</pre>"),
941 nullptr, 2);
942
943 // -------> Shortcut
944 SET_PATTERN(QSL("shortcut"), QString(), PlainText,
945 ki18nc("tag-format-pattern <shortcut> plain",
946 // i18n: KUIT pattern, see the comment to the first of these entries above.
947 "%1"),
948 tagFormatterShortcut, 0);
949 SET_PATTERN(QSL("shortcut"), QString(), RichText,
950 ki18nc("tag-format-pattern <shortcut> rich",
951 // i18n: KUIT pattern, see the comment to the first of these entries above.
952 "<b>%1</b>"),
953 tagFormatterShortcut, 0);
954
955 // -------> Interface
956 SET_PATTERN(QSL("interface"), QString(), PlainText,
957 ki18nc("tag-format-pattern <interface> plain",
958 // i18n: KUIT pattern, see the comment to the first of these entries above.
959 "|%1|"),
960 tagFormatterInterface, 0);
961 SET_PATTERN(QSL("interface"), QString(), RichText,
962 ki18nc("tag-format-pattern <interface> rich",
963 // i18n: KUIT pattern, see the comment to the first of these entries above.
964 "<i>%1</i>"),
965 tagFormatterInterface, 0);
966
967 // -------> Emphasis
968 SET_PATTERN(EMPHASIS, QString(), PlainText,
969 ki18nc("tag-format-pattern <emphasis> plain",
970 // i18n: KUIT pattern, see the comment to the first of these entries above.
971 "*%1*"),
972 nullptr, 0);
973 SET_PATTERN(EMPHASIS, QString(), RichText,
974 ki18nc("tag-format-pattern <emphasis> rich",
975 // i18n: KUIT pattern, see the comment to the first of these entries above.
976 "<i>%1</i>"),
977 nullptr, 0);
978 SET_PATTERN(EMPHASIS, QSL("strong"), PlainText,
979 ki18nc("tag-format-pattern <emphasis-strong> plain",
980 // i18n: KUIT pattern, see the comment to the first of these entries above.
981 "**%1**"),
982 nullptr, 0);
983 SET_PATTERN(EMPHASIS, QSL("strong"), RichText,
984 ki18nc("tag-format-pattern <emphasis-strong> rich",
985 // i18n: KUIT pattern, see the comment to the first of these entries above.
986 "<b>%1</b>"),
987 nullptr, 0);
988
989 // -------> Placeholder
990 SET_PATTERN(QSL("placeholder"), QString(), PlainText,
991 ki18nc("tag-format-pattern <placeholder> plain",
992 // i18n: KUIT pattern, see the comment to the first of these entries above.
993 "&lt;%1&gt;"),
994 nullptr, 0);
995 SET_PATTERN(QSL("placeholder"), QString(), RichText,
996 ki18nc("tag-format-pattern <placeholder> rich",
997 // i18n: KUIT pattern, see the comment to the first of these entries above.
998 "&lt;<i>%1</i>&gt;"),
999 nullptr, 0);
1000
1001 // -------> Email
1002 SET_PATTERN(QSL("email"), QString(), PlainText,
1003 ki18nc("tag-format-pattern <email> plain",
1004 // i18n: KUIT pattern, see the comment to the first of these entries above.
1005 "&lt;%1&gt;"),
1006 nullptr, 0);
1007 SET_PATTERN(QSL("email"), QString(), RichText,
1008 ki18nc("tag-format-pattern <email> rich",
1009 // i18n: KUIT pattern, see the comment to the first of these entries above.
1010 "&lt;<a href=\"mailto:%1\">%1</a>&gt;"),
1011 nullptr, 0);
1012 SET_PATTERN(QSL("email"), QSL("address"), PlainText,
1013 ki18nc("tag-format-pattern <email address=> plain\n"
1014 "%1 is name, %2 is address",
1015 // i18n: KUIT pattern, see the comment to the first of these entries above.
1016 "%1 &lt;%2&gt;"),
1017 nullptr, 0);
1018 SET_PATTERN(QSL("email"), QSL("address"), RichText,
1019 ki18nc("tag-format-pattern <email address=> rich\n"
1020 "%1 is name, %2 is address",
1021 // i18n: KUIT pattern, see the comment to the first of these entries above.
1022 "<a href=\"mailto:%2\">%1</a>"),
1023 nullptr, 0);
1024
1025 // -------> Envar
1026 SET_PATTERN(QSL("envar"), QString(), PlainText,
1027 ki18nc("tag-format-pattern <envar> plain",
1028 // i18n: KUIT pattern, see the comment to the first of these entries above.
1029 "$%1"),
1030 nullptr, 0);
1031 SET_PATTERN(QSL("envar"), QString(), RichText,
1032 ki18nc("tag-format-pattern <envar> rich",
1033 // i18n: KUIT pattern, see the comment to the first of these entries above.
1034 "<tt>$%1</tt>"),
1035 nullptr, 0);
1036
1037 // -------> Message
1038 SET_PATTERN(QSL("message"), QString(), PlainText,
1039 ki18nc("tag-format-pattern <message> plain",
1040 // i18n: KUIT pattern, see the comment to the first of these entries above.
1041 "/%1/"),
1042 nullptr, 0);
1043 SET_PATTERN(QSL("message"), QString(), RichText,
1044 ki18nc("tag-format-pattern <message> rich",
1045 // i18n: KUIT pattern, see the comment to the first of these entries above.
1046 "<i>%1</i>"),
1047 nullptr, 0);
1048
1049 // -------> Nl
1050 SET_PATTERN(QSL("nl"), QString(), PlainText,
1051 ki18nc("tag-format-pattern <nl> plain",
1052 // i18n: KUIT pattern, see the comment to the first of these entries above.
1053 "%1\n"),
1054 nullptr, 0);
1055 SET_PATTERN(QSL("nl"), QString(), RichText,
1056 ki18nc("tag-format-pattern <nl> rich",
1057 // i18n: KUIT pattern, see the comment to the first of these entries above.
1058 "%1<br/>"),
1059 nullptr, 0);
1060 // clang-format on
1061}
1062
1063void KuitSetupPrivate::setDefaultFormats()
1064{
1065 using namespace Kuit;
1066
1067 // Setup formats by role.
1068 formatsByRoleCue[ActionRole][UndefinedCue] = PlainText;
1069 formatsByRoleCue[TitleRole][UndefinedCue] = PlainText;
1070 formatsByRoleCue[LabelRole][UndefinedCue] = PlainText;
1071 formatsByRoleCue[OptionRole][UndefinedCue] = PlainText;
1072 formatsByRoleCue[ItemRole][UndefinedCue] = PlainText;
1073 formatsByRoleCue[InfoRole][UndefinedCue] = RichText;
1074
1075 // Setup override formats by subcue.
1076 formatsByRoleCue[InfoRole][StatusCue] = PlainText;
1077 formatsByRoleCue[InfoRole][ProgressCue] = PlainText;
1078 formatsByRoleCue[InfoRole][CreditCue] = PlainText;
1079 formatsByRoleCue[InfoRole][ShellCue] = TermText;
1080}
1081
1082KuitSetup::KuitSetup(const QByteArray &domain)
1083 : d(new KuitSetupPrivate)
1084{
1085 d->domain = domain;
1086 d->setDefaultMarkup();
1087 d->setDefaultFormats();
1088}
1089
1090KuitSetup::~KuitSetup() = default;
1091
1092void KuitSetup::setTagPattern(const QString &tagName,
1093 const QStringList &attribNames,
1094 Kuit::VisualFormat format,
1095 const KLocalizedString &pattern,
1096 Kuit::TagFormatter formatter,
1097 int leadingNewlines)
1098{
1099 d->setTagPattern(tagName, attribNames_: attribNames, format, pattern, formatter, leadingNewlines_: leadingNewlines);
1100}
1101
1102void KuitSetup::setTagClass(const QString &tagName, Kuit::TagClass aClass)
1103{
1104 d->setTagClass(tagName, aClass);
1105}
1106
1107void KuitSetup::setFormatForMarker(const QString &marker, Kuit::VisualFormat format)
1108{
1109 d->setFormatForMarker(marker, format);
1110}
1111
1112class KuitFormatterPrivate
1113{
1114public:
1115 KuitFormatterPrivate(const QString &language);
1116
1117 QString format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const;
1118
1119 // Get metatranslation (formatting patterns, etc.)
1120 QString metaTr(const char *context, const char *text) const;
1121
1122 // Set visual formatting patterns for text within tags.
1123 void setFormattingPatterns();
1124
1125 // Set data used in transformation of text within tags.
1126 void setTextTransformData();
1127
1128 // Determine visual format by parsing the UI marker in the context.
1129 static Kuit::VisualFormat formatFromUiMarker(const QString &context, const KuitSetup &setup);
1130
1131 // Determine if text has block structure (multiple paragraphs, etc).
1132 static bool determineIsStructured(const QString &text, const KuitSetup &setup);
1133
1134 // Format KUIT text into visual text.
1135 QString toVisualText(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const;
1136
1137 // Final touches to the formatted text.
1138 QString finalizeVisualText(const QString &ftext, Kuit::VisualFormat format) const;
1139
1140 // In case of markup errors, try to make result not look too bad.
1141 QString salvageMarkup(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const;
1142
1143 // Data for XML parsing state.
1144 class OpenEl
1145 {
1146 public:
1147 enum Handling { Proper, Ignored, Dropout };
1148
1149 QString name;
1150 QHash<QString, QString> attributes;
1151 QString attribStr;
1152 Handling handling;
1153 QString formattedText;
1154 QStringList tagPath;
1155 };
1156
1157 // Gather data about current element for the parse state.
1158 KuitFormatterPrivate::OpenEl parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const;
1159
1160 // Format text of the element.
1161 QString formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const;
1162
1163 // Count number of newlines at start and at end of text.
1164 static void countWrappingNewlines(const QString &ptext, int &numle, int &numtr);
1165
1166private:
1167 QString language;
1168 QStringList languageAsList;
1169
1170 QHash<Kuit::VisualFormat, QString> comboKeyDelim;
1171 QHash<Kuit::VisualFormat, QString> guiPathDelim;
1172
1173 QHash<QString, QString> keyNames;
1174};
1175
1176KuitFormatterPrivate::KuitFormatterPrivate(const QString &language_)
1177 : language(language_)
1178{
1179}
1180
1181QString KuitFormatterPrivate::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const
1182{
1183 const KuitSetup &setup = Kuit::setupForDomain(domain);
1184
1185 // If format is undefined, determine it based on UI marker inside context.
1186 Kuit::VisualFormat resolvedFormat = format;
1187 if (resolvedFormat == Kuit::UndefinedFormat) {
1188 resolvedFormat = formatFromUiMarker(context, setup);
1189 }
1190
1191 // Quick check: are there any tags at all?
1192 QString ftext;
1193 if (text.indexOf(QL1C('<')) < 0) {
1194 ftext = finalizeVisualText(ftext: text, format: resolvedFormat);
1195 } else {
1196 // Format the text.
1197 ftext = toVisualText(text, format: resolvedFormat, setup);
1198 if (ftext.isEmpty()) { // error while processing markup
1199 ftext = salvageMarkup(text, format: resolvedFormat, setup);
1200 }
1201 }
1202 return ftext;
1203}
1204
1205Kuit::VisualFormat KuitFormatterPrivate::formatFromUiMarker(const QString &context, const KuitSetup &setup)
1206{
1207 KuitStaticData *s = staticData();
1208
1209 QString roleName;
1210 QString cueName;
1211 QString formatName;
1212 parseUiMarker(context_: context, roleName, cueName, formatName);
1213
1214 // Set role from name.
1215 Kuit::Role role = s->rolesByName.value(key: roleName, defaultValue: Kuit::UndefinedRole);
1216 if (role == Kuit::UndefinedRole) { // unknown role
1217 if (!roleName.isEmpty()) {
1218 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker in context {%2}.").arg(args&: roleName, args: shorten(str: context));
1219 }
1220 }
1221
1222 // Set subcue from name.
1223 Kuit::Cue cue;
1224 if (role != Kuit::UndefinedRole) {
1225 cue = s->cuesByName.value(key: cueName, defaultValue: Kuit::UndefinedCue);
1226 if (cue != Kuit::UndefinedCue) { // known subcue
1227 if (!s->knownRoleCues.value(key: role).contains(value: cue)) {
1228 cue = Kuit::UndefinedCue;
1229 qCWarning(KI18N_KUIT)
1230 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker in context {%3}.").arg(args&: cueName, args&: roleName, args: shorten(str: context));
1231 }
1232 } else { // unknown or not given subcue
1233 if (!cueName.isEmpty()) {
1234 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker in context {%2}.").arg(args&: cueName, args: shorten(str: context));
1235 }
1236 }
1237 } else {
1238 // Bad role, silently ignore the cue.
1239 cue = Kuit::UndefinedCue;
1240 }
1241
1242 // Set format from name, or by derivation from context/subcue.
1243 Kuit::VisualFormat format = s->formatsByName.value(key: formatName, defaultValue: Kuit::UndefinedFormat);
1244 if (format == Kuit::UndefinedFormat) { // unknown or not given format
1245 // Check first if there is a format defined for role/subcue
1246 // combination, then for role only, otherwise default to undefined.
1247 auto formatsByCueIt = setup.d->formatsByRoleCue.constFind(key: role);
1248 if (formatsByCueIt != setup.d->formatsByRoleCue.constEnd()) {
1249 const auto &formatsByCue = *formatsByCueIt;
1250 auto formatIt = formatsByCue.constFind(key: cue);
1251 if (formatIt != formatsByCue.constEnd()) {
1252 format = *formatIt;
1253 } else {
1254 format = formatsByCue.value(key: Kuit::UndefinedCue);
1255 }
1256 }
1257 if (!formatName.isEmpty()) {
1258 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown format '/%1' in UI marker for message {%2}.").arg(args&: formatName, args: shorten(str: context));
1259 }
1260 }
1261 if (format == Kuit::UndefinedFormat) {
1262 format = Kuit::PlainText;
1263 }
1264
1265 return format;
1266}
1267
1268bool KuitFormatterPrivate::determineIsStructured(const QString &text, const KuitSetup &setup)
1269{
1270 // If the text opens with a structuring tag, then it is structured,
1271 // otherwise not. Leading whitespace is ignored for this purpose.
1272 static const QRegularExpression opensWithTagRx(QStringLiteral("^\\s*<\\s*(\\w+)[^>]*>"));
1273 bool isStructured = false;
1274 const QRegularExpressionMatch match = opensWithTagRx.match(subject: text);
1275 if (match.hasMatch()) {
1276 const QString tagName = match.captured(nth: 1).toLower();
1277 auto tagIt = setup.d->knownTags.constFind(key: tagName);
1278 if (tagIt != setup.d->knownTags.constEnd()) {
1279 const KuitTag &tag = *tagIt;
1280 isStructured = (tag.type == Kuit::StructTag);
1281 }
1282 }
1283 return isStructured;
1284}
1285
1286static const char s_entitySubRx[] = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+";
1287
1288QString KuitFormatterPrivate::toVisualText(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const
1289{
1290 KuitStaticData *s = staticData();
1291
1292 // Replace &-shortcut marker with "&amp;", not to confuse the parser;
1293 // but do not touch & which forms an XML entity as it is.
1294 QString original = text_;
1295 // Regex is (see s_entitySubRx var): ^([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);
1296 static const QRegularExpression restRx(QLatin1String("^(") + QLatin1String(s_entitySubRx) + QLatin1String(");"));
1297
1298 QString text;
1299 int p = original.indexOf(QL1C('&'));
1300 while (p >= 0) {
1301 text.append(v: QStringView(original).mid(pos: 0, n: p + 1));
1302 original.remove(i: 0, len: p + 1);
1303 if (original.indexOf(re: restRx) != 0) { // not an entity
1304 text.append(QSL("amp;"));
1305 }
1306 p = original.indexOf(QL1C('&'));
1307 }
1308 text.append(s: original);
1309
1310 // FIXME: Do this and then check proper use of structuring and phrase tags.
1311#if 0
1312 // Determine whether this is block-structured text.
1313 bool isStructured = determineIsStructured(text, setup);
1314#endif
1315
1316 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__");
1317 // Add top tag, not to confuse the parser.
1318 text = QStringLiteral("<%2>%1</%2>").arg(args&: text, args: INTERNAL_TOP_TAG_NAME);
1319
1320 QStack<OpenEl> openEls;
1321 QXmlStreamReader xml(text);
1322 xml.setEntityResolver(&s->xmlEntityResolver);
1323 QStringView lastElementName;
1324
1325 while (!xml.atEnd()) {
1326 xml.readNext();
1327
1328 if (xml.isStartElement()) {
1329 lastElementName = xml.name();
1330
1331 OpenEl oel;
1332
1333 if (openEls.isEmpty()) {
1334 // Must be the root element.
1335 oel.name = INTERNAL_TOP_TAG_NAME;
1336 oel.handling = OpenEl::Proper;
1337 } else {
1338 // Find first proper enclosing element.
1339 OpenEl enclosingOel;
1340 for (int i = openEls.size() - 1; i >= 0; --i) {
1341 if (openEls[i].handling == OpenEl::Proper) {
1342 enclosingOel = openEls[i];
1343 break;
1344 }
1345 }
1346 // Collect data about this element.
1347 oel = parseOpenEl(xml, enclosingOel, text, setup);
1348 }
1349
1350 // Record the new element on the parse stack.
1351 openEls.push(t: oel);
1352 } else if (xml.isEndElement()) {
1353 // Get closed element data.
1354 OpenEl oel = openEls.pop();
1355
1356 // If this was closing of the top element, we're done.
1357 if (openEls.isEmpty()) {
1358 // Return with final touches applied.
1359 return finalizeVisualText(ftext: oel.formattedText, format);
1360 }
1361
1362 // Append formatted text segment.
1363 QString ptext = openEls.top().formattedText; // preceding text
1364 openEls.top().formattedText += formatSubText(ptext, oel, format, setup);
1365 } else if (xml.isCharacters()) {
1366 // Stream reader will automatically resolve default XML entities,
1367 // which is not desired in this case, as the entities are to be
1368 // resolved in finalizeVisualText. Convert back into entities.
1369 const QString ctext = xml.text().toString();
1370 QString nctext;
1371 for (const QChar c : ctext) {
1372 auto nameIt = s->xmlEntitiesInverse.constFind(key: c);
1373 if (nameIt != s->xmlEntitiesInverse.constEnd()) {
1374 const QString &entName = *nameIt;
1375 nctext += QL1C('&') + entName + QL1C(';');
1376 } else {
1377 nctext += c;
1378 }
1379 }
1380 openEls.top().formattedText += nctext;
1381 }
1382 }
1383
1384 if (xml.hasError()) {
1385 qCWarning(KI18N_KUIT) << QStringLiteral("Markup error in message {%1}: %2. Last tag parsed: %3. Complete message follows:\n%4")
1386 .arg(args: shorten(str: text), args: xml.errorString(), args: lastElementName.toString(), args&: text);
1387 return QString();
1388 }
1389
1390 // Cannot reach here.
1391 return text;
1392}
1393
1394KuitFormatterPrivate::OpenEl
1395KuitFormatterPrivate::parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const
1396{
1397 OpenEl oel;
1398 oel.name = xml.name().toString().toLower();
1399
1400 // Collect attribute names and values, and format attribute string.
1401 QStringList attribNames;
1402 QStringList attribValues;
1403 const auto listAttributes = xml.attributes();
1404 attribNames.reserve(asize: listAttributes.size());
1405 attribValues.reserve(asize: listAttributes.size());
1406 for (const QXmlStreamAttribute &xatt : listAttributes) {
1407 attribNames += xatt.name().toString().toLower();
1408 attribValues += xatt.value().toString();
1409 QChar qc = attribValues.last().indexOf(QL1C('\'')) < 0 ? QL1C('\'') : QL1C('"');
1410 oel.attribStr += QL1C(' ') + attribNames.last() + QL1C('=') + qc + attribValues.last() + qc;
1411 }
1412
1413 auto tagIt = setup.d->knownTags.constFind(key: oel.name);
1414 if (tagIt != setup.d->knownTags.constEnd()) { // known KUIT element
1415 const KuitTag &tag = *tagIt;
1416 const KuitTag &etag = setup.d->knownTags.value(key: enclosingOel.name);
1417
1418 // If this element can be contained within enclosing element,
1419 // mark it proper, otherwise mark it for removal.
1420 if (tag.name.isEmpty() || tag.type == Kuit::PhraseTag || etag.type == Kuit::StructTag) {
1421 oel.handling = OpenEl::Proper;
1422 } else {
1423 oel.handling = OpenEl::Dropout;
1424 qCWarning(KI18N_KUIT)
1425 << QStringLiteral("Structuring tag ('%1') cannot be subtag of phrase tag ('%2') in message {%3}.").arg(args: tag.name, args: etag.name, args: shorten(str: text));
1426 }
1427
1428 // Resolve attributes and compute attribute set key.
1429 QSet<QString> attset;
1430 for (int i = 0; i < attribNames.size(); ++i) {
1431 QString att = attribNames[i];
1432 if (tag.knownAttribs.contains(value: att)) {
1433 attset << att;
1434 oel.attributes[att] = attribValues[i];
1435 } else {
1436 qCWarning(KI18N_KUIT) << QStringLiteral("Attribute '%1' not defined for tag '%2' in message {%3}.").arg(args&: att, args: tag.name, args: shorten(str: text));
1437 }
1438 }
1439
1440 // Continue tag path.
1441 oel.tagPath = enclosingOel.tagPath;
1442 oel.tagPath.prepend(t: enclosingOel.name);
1443
1444 } else { // unknown element, leave it in verbatim
1445 oel.handling = OpenEl::Ignored;
1446 qCWarning(KI18N_KUIT) << QStringLiteral("Tag '%1' is not defined in message {%2}.").arg(args&: oel.name, args: shorten(str: text));
1447 }
1448
1449 return oel;
1450}
1451
1452QString KuitFormatterPrivate::formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const
1453{
1454 if (oel.handling == OpenEl::Proper) {
1455 const KuitTag &tag = setup.d->knownTags.value(key: oel.name);
1456 QString ftext = tag.format(languages: languageAsList, attributes: oel.attributes, text: oel.formattedText, tagPath: oel.tagPath, format);
1457
1458 // Handle leading newlines, if this is not start of the text
1459 // (ptext is the preceding text).
1460 if (!ptext.isEmpty() && tag.leadingNewlines > 0) {
1461 // Count number of present newlines.
1462 int pnumle;
1463 int pnumtr;
1464 int fnumle;
1465 int fnumtr;
1466 countWrappingNewlines(ptext, numle&: pnumle, numtr&: pnumtr);
1467 countWrappingNewlines(ptext: ftext, numle&: fnumle, numtr&: fnumtr);
1468 // Number of leading newlines already present.
1469 int numle = pnumtr + fnumle;
1470 // The required extra newlines.
1471 QString strle;
1472 if (numle < tag.leadingNewlines) {
1473 strle = QString(tag.leadingNewlines - numle, QL1C('\n'));
1474 }
1475 ftext = strle + ftext;
1476 }
1477
1478 return ftext;
1479
1480 } else if (oel.handling == OpenEl::Ignored) {
1481 return QL1C('<') + oel.name + oel.attribStr + QL1C('>') + oel.formattedText + QSL("</") + oel.name + QL1C('>');
1482
1483 } else { // oel.handling == OpenEl::Dropout
1484 return oel.formattedText;
1485 }
1486}
1487
1488void KuitFormatterPrivate::countWrappingNewlines(const QString &text, int &numle, int &numtr)
1489{
1490 int len = text.length();
1491 // Number of newlines at start of text.
1492 numle = 0;
1493 while (numle < len && text[numle] == QL1C('\n')) {
1494 ++numle;
1495 }
1496 // Number of newlines at end of text.
1497 numtr = 0;
1498 while (numtr < len && text[len - numtr - 1] == QL1C('\n')) {
1499 ++numtr;
1500 }
1501}
1502
1503QString KuitFormatterPrivate::finalizeVisualText(const QString &text_, Kuit::VisualFormat format) const
1504{
1505 KuitStaticData *s = staticData();
1506
1507 QString text = text_;
1508
1509 // Resolve XML entities.
1510 if (format != Kuit::RichText) {
1511 // regex is (see s_entitySubRx var): (&([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);)
1512 static const QRegularExpression entRx(QLatin1String("(&(") + QLatin1String(s_entitySubRx) + QLatin1String(");)"));
1513 QRegularExpressionMatch match;
1514 QString plain;
1515 while ((match = entRx.match(subject: text)).hasMatch()) {
1516 plain.append(v: QStringView(text).mid(pos: 0, n: match.capturedStart(nth: 0)));
1517 text.remove(i: 0, len: match.capturedEnd(nth: 0));
1518 const QString ent = match.captured(nth: 2);
1519 if (ent.startsWith(QL1C('#'))) { // numeric character entity
1520 bool ok;
1521 QStringView entView(ent);
1522 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));
1523 if (ok) {
1524 plain.append(c);
1525 } else { // unknown Unicode point, leave as is
1526 plain.append(v: match.capturedView(nth: 0));
1527 }
1528 } else if (s->xmlEntities.contains(key: ent)) { // known entity
1529 plain.append(s: s->xmlEntities[ent]);
1530 } else { // unknown entity, just leave as is
1531 plain.append(v: match.capturedView(nth: 0));
1532 }
1533 }
1534 plain.append(s: text);
1535 text = plain;
1536 }
1537
1538 // Add top tag.
1539 if (format == Kuit::RichText) {
1540 text = QLatin1String("<html>") + text + QLatin1String("</html>");
1541 }
1542
1543 return text;
1544}
1545
1546QString KuitFormatterPrivate::salvageMarkup(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const
1547{
1548 QString text = text_;
1549 QString ntext;
1550
1551 // Resolve tags simple-mindedly.
1552
1553 // - tags with content
1554 static const QRegularExpression wrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)"), QRegularExpression::InvertedGreedinessOption);
1555 QRegularExpressionMatchIterator iter = wrapRx.globalMatch(subject: text);
1556 QRegularExpressionMatch match;
1557 int pos = 0;
1558 while (iter.hasNext()) {
1559 match = iter.next();
1560 ntext += QStringView(text).mid(pos, n: match.capturedStart(nth: 0) - pos);
1561 const QString tagname = match.captured(nth: 2).toLower();
1562 const QString content = salvageMarkup(text_: match.captured(nth: 4), format, setup);
1563 auto tagIt = setup.d->knownTags.constFind(key: tagname);
1564 if (tagIt != setup.d->knownTags.constEnd()) {
1565 const KuitTag &tag = *tagIt;
1566 QHash<QString, QString> attributes;
1567 // TODO: Do not ignore attributes (in match.captured(3)).
1568 ntext += tag.format(languages: languageAsList, attributes, text: content, tagPath: QStringList(), format);
1569 } else {
1570 ntext += match.captured(nth: 1) + content + match.captured(nth: 5);
1571 }
1572 pos = match.capturedEnd(nth: 0);
1573 }
1574 // get the remaining part after the last match in "text"
1575 ntext += QStringView(text).mid(pos);
1576 text = ntext;
1577
1578 // - tags without content
1579 static const QRegularExpression nowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>"), QRegularExpression::InvertedGreedinessOption);
1580 iter = nowrRx.globalMatch(subject: text);
1581 pos = 0;
1582 ntext.clear();
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: 1).toLower();
1587 auto tagIt = setup.d->knownTags.constFind(key: tagname);
1588 if (tagIt != setup.d->knownTags.constEnd()) {
1589 const KuitTag &tag = *tagIt;
1590 ntext += tag.format(languages: languageAsList, attributes: QHash<QString, QString>(), text: QString(), tagPath: QStringList(), format);
1591 } else {
1592 ntext += match.captured(nth: 0);
1593 }
1594 pos = match.capturedEnd(nth: 0);
1595 }
1596 // get the remaining part after the last match in "text"
1597 ntext += QStringView(text).mid(pos);
1598 text = ntext;
1599
1600 // Add top tag.
1601 if (format == Kuit::RichText) {
1602 text = QStringLiteral("<html>") + text + QStringLiteral("</html>");
1603 }
1604
1605 return text;
1606}
1607
1608KuitFormatter::KuitFormatter(const QString &language)
1609 : d(new KuitFormatterPrivate(language))
1610{
1611}
1612
1613KuitFormatter::~KuitFormatter()
1614{
1615 delete d;
1616}
1617
1618QString KuitFormatter::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const
1619{
1620 return d->format(domain, context, text, format);
1621}
1622

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