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