KI18n

kuitsetup.cpp
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(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(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(1, wsRx.match(context).capturedStart(0) - 1);
72
73 // Possible format.
74 int pfmt = context.indexOf(QL1C('/'));
75 if (pfmt >= 0) {
76 formatName = context.mid(pfmt + 1);
77 context.truncate(pfmt);
78 }
79
80 // Possible subcue.
81 int pcue = context.indexOf(QL1C(':'));
82 if (pcue >= 0) {
83 cueName = context.mid(pcue + 1);
84 context.truncate(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(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;
172
176
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(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(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("keyboard-key-name", "Alt"));
335 setKeyName(kli18nc("keyboard-key-name", "AltGr"));
336 setKeyName(kli18nc("keyboard-key-name", "Backspace"));
337 setKeyName(kli18nc("keyboard-key-name", "CapsLock"));
338 setKeyName(kli18nc("keyboard-key-name", "Control"));
339 setKeyName(kli18nc("keyboard-key-name", "Ctrl"));
340 setKeyName(kli18nc("keyboard-key-name", "Del"));
341 setKeyName(kli18nc("keyboard-key-name", "Delete"));
342 setKeyName(kli18nc("keyboard-key-name", "Down"));
343 setKeyName(kli18nc("keyboard-key-name", "End"));
344 setKeyName(kli18nc("keyboard-key-name", "Enter"));
345 setKeyName(kli18nc("keyboard-key-name", "Esc"));
346 setKeyName(kli18nc("keyboard-key-name", "Escape"));
347 setKeyName(kli18nc("keyboard-key-name", "Home"));
348 setKeyName(kli18nc("keyboard-key-name", "Hyper"));
349 setKeyName(kli18nc("keyboard-key-name", "Ins"));
350 setKeyName(kli18nc("keyboard-key-name", "Insert"));
351 setKeyName(kli18nc("keyboard-key-name", "Left"));
352 setKeyName(kli18nc("keyboard-key-name", "Menu"));
353 setKeyName(kli18nc("keyboard-key-name", "Meta"));
354 setKeyName(kli18nc("keyboard-key-name", "NumLock"));
355 setKeyName(kli18nc("keyboard-key-name", "PageDown"));
356 setKeyName(kli18nc("keyboard-key-name", "PageUp"));
357 setKeyName(kli18nc("keyboard-key-name", "PgDown"));
358 setKeyName(kli18nc("keyboard-key-name", "PgUp"));
359 setKeyName(kli18nc("keyboard-key-name", "PauseBreak"));
360 setKeyName(kli18nc("keyboard-key-name", "PrintScreen"));
361 setKeyName(kli18nc("keyboard-key-name", "PrtScr"));
362 setKeyName(kli18nc("keyboard-key-name", "Return"));
363 setKeyName(kli18nc("keyboard-key-name", "Right"));
364 setKeyName(kli18nc("keyboard-key-name", "ScrollLock"));
365 setKeyName(kli18nc("keyboard-key-name", "Shift"));
366 setKeyName(kli18nc("keyboard-key-name", "Space"));
367 setKeyName(kli18nc("keyboard-key-name", "Super"));
368 setKeyName(kli18nc("keyboard-key-name", "SysReq"));
369 setKeyName(kli18nc("keyboard-key-name", "Tab"));
370 setKeyName(kli18nc("keyboard-key-name", "Up"));
371 setKeyName(kli18nc("keyboard-key-name", "Win"));
372 setKeyName(kli18nc("keyboard-key-name", "F1"));
373 setKeyName(kli18nc("keyboard-key-name", "F2"));
374 setKeyName(kli18nc("keyboard-key-name", "F3"));
375 setKeyName(kli18nc("keyboard-key-name", "F4"));
376 setKeyName(kli18nc("keyboard-key-name", "F5"));
377 setKeyName(kli18nc("keyboard-key-name", "F6"));
378 setKeyName(kli18nc("keyboard-key-name", "F7"));
379 setKeyName(kli18nc("keyboard-key-name", "F8"));
380 setKeyName(kli18nc("keyboard-key-name", "F9"));
381 setKeyName(kli18nc("keyboard-key-name", "F10"));
382 setKeyName(kli18nc("keyboard-key-name", "F11"));
383 setKeyName(kli18nc("keyboard-key-name", "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(shstr);
395 QStringList keys;
396 if (match.hasMatch()) { // delimiter found, multi-key shortcut
397 const QString oldDelim = match.captured(0);
398 keys = shstr.split(oldDelim, Qt::SkipEmptyParts);
399 } else { // single-key shortcut, no delimiter found
400 keys.append(shstr);
401 }
402
403 for (QString &key : keys) {
404 // Normalize key
405 key = key.trimmed();
406 auto nameIt = keyNames.constFind(key.toLower());
407 if (nameIt != keyNames.constEnd()) {
408 key = nameIt->toString(languages);
409 }
410 }
411 const QString delim = comboKeyDelim.value(format).toString(languages);
412 return keys.join(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(inpstr);
421 if (match.hasMatch()) { // multi-element path
422 const QString oldDelim = match.captured(0);
423 QStringList guiels = inpstr.split(oldDelim, Qt::SkipEmptyParts);
424 const QString delim = guiPathDelim.value(format).toString(languages);
425 return guiels.join(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(attribNames.begin(), 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;
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(attributes.keys());
476 const QHash<Kuit::VisualFormat, KLocalizedString> pattern = patterns.value(attribKey);
477 auto patternIt = pattern.constFind(format);
478 if (patternIt != pattern.constEnd()) {
479 QString modText;
480 Kuit::TagFormatter formatter = formatters.value(attribKey).value(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(modText);
492 const QStringList attributeOrder = attributeOrders.value(attribKey).value(format);
493 for (const QString &attribName : attributeOrder) {
494 aggText = aggText.subs(attributes.value(attribName));
495 }
496 formattedText = aggText.ignoreMarkup().toString(languages);
497 } else {
498 formattedText = modText;
499 }
500 } else if (patterns.contains(attribKey)) {
501 qCWarning(KI18N_KUIT)
502 << QStringLiteral("Undefined visual format for tag <%1> and attribute combination %2: %3.").arg(name, attribKey, s->namesByFormat.value(format));
503 } else {
504 qCWarning(KI18N_KUIT) << QStringLiteral("Undefined attribute combination for tag <%1>: %2.").arg(name, attribKey);
505 }
506 return formattedText;
507}
508
510{
511 KuitStaticData *s = staticData();
512 KuitSetup *setup = s->domainSetups.value(domain);
513 if (!setup) {
514 setup = new KuitSetup(domain);
515 s->domainSetups.insert(domain, 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;
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(tagName);
550 if (tagIt == knownTags.end()) {
551 tagIt = knownTags.insert(tagName, KuitTag(tagName, Kuit::PhraseTag));
552 }
553
554 KuitTag &tag = *tagIt;
555
556 QStringList attribNames = attribNames_;
557 attribNames.removeAll(QString());
558 for (const QString &attribName : std::as_const(attribNames)) {
559 tag.knownAttribs.insert(attribName);
560 }
561 QString attribKey = attributeSetKey(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(tagName);
571 if (tagIt == knownTags.end()) {
572 knownTags.insert(tagName, 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(marker, roleName, cueName, formatName);
586
587 Kuit::Role role;
588 auto roleIt = s->rolesByName.constFind(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(roleName, marker);
593 return;
594 } else {
595 qCWarning(KI18N_KUIT) << QStringLiteral("Empty role in UI marker {%1}, visual format not set.").arg(marker);
596 return;
597 }
598
599 Kuit::Cue cue;
600 auto cueIt = s->cuesByName.constFind(cueName);
601 if (cueIt != s->cuesByName.constEnd()) {
602 cue = *cueIt;
603 if (!s->knownRoleCues.value(role).contains(cue)) {
604 qCWarning(KI18N_KUIT)
605 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker {%3}, visual format not set.").arg(cueName, roleName, 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(cueName, 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(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, 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, 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(INTERNAL_TOP_TAG_NAME, 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(TITLE, 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"), 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"), 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"), 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"), 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
1091{
1092 delete d;
1093}
1094
1096 const QStringList &attribNames,
1097 Kuit::VisualFormat format,
1098 const KLocalizedString &pattern,
1099 Kuit::TagFormatter formatter,
1100 int leadingNewlines)
1101{
1102 d->setTagPattern(tagName, attribNames, format, pattern, formatter, leadingNewlines);
1103}
1104
1106{
1107 d->setTagClass(tagName, aClass);
1108}
1109
1111{
1112 d->setFormatForMarker(marker, format);
1113}
1114
1115class KuitFormatterPrivate
1116{
1117public:
1118 KuitFormatterPrivate(const QString &language);
1119
1120 QString format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const;
1121
1122 // Get metatranslation (formatting patterns, etc.)
1123 QString metaTr(const char *context, const char *text) const;
1124
1125 // Set visual formatting patterns for text within tags.
1126 void setFormattingPatterns();
1127
1128 // Set data used in transformation of text within tags.
1129 void setTextTransformData();
1130
1131 // Determine visual format by parsing the UI marker in the context.
1132 static Kuit::VisualFormat formatFromUiMarker(const QString &context, const KuitSetup &setup);
1133
1134 // Determine if text has block structure (multiple paragraphs, etc).
1135 static bool determineIsStructured(const QString &text, const KuitSetup &setup);
1136
1137 // Format KUIT text into visual text.
1138 QString toVisualText(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const;
1139
1140 // Final touches to the formatted text.
1141 QString finalizeVisualText(const QString &ftext, Kuit::VisualFormat format) const;
1142
1143 // In case of markup errors, try to make result not look too bad.
1144 QString salvageMarkup(const QString &text, Kuit::VisualFormat format, const KuitSetup &setup) const;
1145
1146 // Data for XML parsing state.
1147 class OpenEl
1148 {
1149 public:
1150 enum Handling { Proper, Ignored, Dropout };
1151
1152 QString name;
1153 QHash<QString, QString> attributes;
1154 QString attribStr;
1155 Handling handling;
1156 QString formattedText;
1157 QStringList tagPath;
1158 };
1159
1160 // Gather data about current element for the parse state.
1161 KuitFormatterPrivate::OpenEl parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const;
1162
1163 // Format text of the element.
1164 QString formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const;
1165
1166 // Count number of newlines at start and at end of text.
1167 static void countWrappingNewlines(const QString &ptext, int &numle, int &numtr);
1168
1169private:
1170 QString language;
1171 QStringList languageAsList;
1172
1175
1176 QHash<QString, QString> keyNames;
1177};
1178
1179KuitFormatterPrivate::KuitFormatterPrivate(const QString &language_)
1180 : language(language_)
1181{
1182}
1183
1184QString KuitFormatterPrivate::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const
1185{
1186 const KuitSetup &setup = Kuit::setupForDomain(domain);
1187
1188 // If format is undefined, determine it based on UI marker inside context.
1189 Kuit::VisualFormat resolvedFormat = format;
1190 if (resolvedFormat == Kuit::UndefinedFormat) {
1191 resolvedFormat = formatFromUiMarker(context, setup);
1192 }
1193
1194 // Quick check: are there any tags at all?
1195 QString ftext;
1196 if (text.indexOf(QL1C('<')) < 0) {
1197 ftext = finalizeVisualText(text, resolvedFormat);
1198 } else {
1199 // Format the text.
1200 ftext = toVisualText(text, resolvedFormat, setup);
1201 if (ftext.isEmpty()) { // error while processing markup
1202 ftext = salvageMarkup(text, resolvedFormat, setup);
1203 }
1204 }
1205 return ftext;
1206}
1207
1208Kuit::VisualFormat KuitFormatterPrivate::formatFromUiMarker(const QString &context, const KuitSetup &setup)
1209{
1210 KuitStaticData *s = staticData();
1211
1212 QString roleName;
1213 QString cueName;
1214 QString formatName;
1215 parseUiMarker(context, roleName, cueName, formatName);
1216
1217 // Set role from name.
1218 Kuit::Role role = s->rolesByName.value(roleName, Kuit::UndefinedRole);
1219 if (role == Kuit::UndefinedRole) { // unknown role
1220 if (!roleName.isEmpty()) {
1221 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown role '@%1' in UI marker in context {%2}.").arg(roleName, shorten(context));
1222 }
1223 }
1224
1225 // Set subcue from name.
1226 Kuit::Cue cue;
1227 if (role != Kuit::UndefinedRole) {
1228 cue = s->cuesByName.value(cueName, Kuit::UndefinedCue);
1229 if (cue != Kuit::UndefinedCue) { // known subcue
1230 if (!s->knownRoleCues.value(role).contains(cue)) {
1231 cue = Kuit::UndefinedCue;
1232 qCWarning(KI18N_KUIT)
1233 << QStringLiteral("Subcue ':%1' does not belong to role '@%2' in UI marker in context {%3}.").arg(cueName, roleName, shorten(context));
1234 }
1235 } else { // unknown or not given subcue
1236 if (!cueName.isEmpty()) {
1237 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown subcue ':%1' in UI marker in context {%2}.").arg(cueName, shorten(context));
1238 }
1239 }
1240 } else {
1241 // Bad role, silently ignore the cue.
1242 cue = Kuit::UndefinedCue;
1243 }
1244
1245 // Set format from name, or by derivation from context/subcue.
1246 Kuit::VisualFormat format = s->formatsByName.value(formatName, Kuit::UndefinedFormat);
1247 if (format == Kuit::UndefinedFormat) { // unknown or not given format
1248 // Check first if there is a format defined for role/subcue
1249 // combination, then for role only, otherwise default to undefined.
1250 auto formatsByCueIt = setup.d->formatsByRoleCue.constFind(role);
1251 if (formatsByCueIt != setup.d->formatsByRoleCue.constEnd()) {
1252 const auto &formatsByCue = *formatsByCueIt;
1253 auto formatIt = formatsByCue.constFind(cue);
1254 if (formatIt != formatsByCue.constEnd()) {
1255 format = *formatIt;
1256 } else {
1257 format = formatsByCue.value(Kuit::UndefinedCue);
1258 }
1259 }
1260 if (!formatName.isEmpty()) {
1261 qCWarning(KI18N_KUIT) << QStringLiteral("Unknown format '/%1' in UI marker for message {%2}.").arg(formatName, shorten(context));
1262 }
1263 }
1264 if (format == Kuit::UndefinedFormat) {
1265 format = Kuit::PlainText;
1266 }
1267
1268 return format;
1269}
1270
1271bool KuitFormatterPrivate::determineIsStructured(const QString &text, const KuitSetup &setup)
1272{
1273 // If the text opens with a structuring tag, then it is structured,
1274 // otherwise not. Leading whitespace is ignored for this purpose.
1275 static const QRegularExpression opensWithTagRx(QStringLiteral("^\\s*<\\s*(\\w+)[^>]*>"));
1276 bool isStructured = false;
1277 const QRegularExpressionMatch match = opensWithTagRx.match(text);
1278 if (match.hasMatch()) {
1279 const QString tagName = match.captured(1).toLower();
1280 auto tagIt = setup.d->knownTags.constFind(tagName);
1281 if (tagIt != setup.d->knownTags.constEnd()) {
1282 const KuitTag &tag = *tagIt;
1283 isStructured = (tag.type == Kuit::StructTag);
1284 }
1285 }
1286 return isStructured;
1287}
1288
1289static const char s_entitySubRx[] = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+";
1290
1291QString KuitFormatterPrivate::toVisualText(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const
1292{
1293 KuitStaticData *s = staticData();
1294
1295 // Replace &-shortcut marker with "&amp;", not to confuse the parser;
1296 // but do not touch & which forms an XML entity as it is.
1297 QString original = text_;
1298 // Regex is (see s_entitySubRx var): ^([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);
1299 static const QRegularExpression restRx(QLatin1String("^(") + QLatin1String(s_entitySubRx) + QLatin1String(");"));
1300
1301 QString text;
1302 int p = original.indexOf(QL1C('&'));
1303 while (p >= 0) {
1304 text.append(QStringView(original).mid(0, p + 1));
1305 original.remove(0, p + 1);
1306 if (original.indexOf(restRx) != 0) { // not an entity
1307 text.append(QSL("amp;"));
1308 }
1309 p = original.indexOf(QL1C('&'));
1310 }
1311 text.append(original);
1312
1313 // FIXME: Do this and then check proper use of structuring and phrase tags.
1314#if 0
1315 // Determine whether this is block-structured text.
1316 bool isStructured = determineIsStructured(text, setup);
1317#endif
1318
1319 const QString INTERNAL_TOP_TAG_NAME = QStringLiteral("__kuit_internal_top__");
1320 // Add top tag, not to confuse the parser.
1321 text = QStringLiteral("<%2>%1</%2>").arg(text, INTERNAL_TOP_TAG_NAME);
1322
1323 QStack<OpenEl> openEls;
1324 QXmlStreamReader xml(text);
1325 xml.setEntityResolver(&s->xmlEntityResolver);
1326 QStringView lastElementName;
1327
1328 while (!xml.atEnd()) {
1329 xml.readNext();
1330
1331 if (xml.isStartElement()) {
1332 lastElementName = xml.name();
1333
1334 OpenEl oel;
1335
1336 if (openEls.isEmpty()) {
1337 // Must be the root element.
1338 oel.name = INTERNAL_TOP_TAG_NAME;
1339 oel.handling = OpenEl::Proper;
1340 } else {
1341 // Find first proper enclosing element.
1342 OpenEl enclosingOel;
1343 for (int i = openEls.size() - 1; i >= 0; --i) {
1344 if (openEls[i].handling == OpenEl::Proper) {
1345 enclosingOel = openEls[i];
1346 break;
1347 }
1348 }
1349 // Collect data about this element.
1350 oel = parseOpenEl(xml, enclosingOel, text, setup);
1351 }
1352
1353 // Record the new element on the parse stack.
1354 openEls.push(oel);
1355 } else if (xml.isEndElement()) {
1356 // Get closed element data.
1357 OpenEl oel = openEls.pop();
1358
1359 // If this was closing of the top element, we're done.
1360 if (openEls.isEmpty()) {
1361 // Return with final touches applied.
1362 return finalizeVisualText(oel.formattedText, format);
1363 }
1364
1365 // Append formatted text segment.
1366 QString ptext = openEls.top().formattedText; // preceding text
1367 openEls.top().formattedText += formatSubText(ptext, oel, format, setup);
1368 } else if (xml.isCharacters()) {
1369 // Stream reader will automatically resolve default XML entities,
1370 // which is not desired in this case, as the entities are to be
1371 // resolved in finalizeVisualText. Convert back into entities.
1372 const QString ctext = xml.text().toString();
1373 QString nctext;
1374 for (const QChar c : ctext) {
1375 auto nameIt = s->xmlEntitiesInverse.constFind(c);
1376 if (nameIt != s->xmlEntitiesInverse.constEnd()) {
1377 const QString &entName = *nameIt;
1378 nctext += QL1C('&') + entName + QL1C(';');
1379 } else {
1380 nctext += c;
1381 }
1382 }
1383 openEls.top().formattedText += nctext;
1384 }
1385 }
1386
1387 if (xml.hasError()) {
1388 qCWarning(KI18N_KUIT) << QStringLiteral("Markup error in message {%1}: %2. Last tag parsed: %3. Complete message follows:\n%4")
1389 .arg(shorten(text), xml.errorString(), lastElementName.toString(), text);
1390 return QString();
1391 }
1392
1393 // Cannot reach here.
1394 return text;
1395}
1396
1397KuitFormatterPrivate::OpenEl
1398KuitFormatterPrivate::parseOpenEl(const QXmlStreamReader &xml, const OpenEl &enclosingOel, const QString &text, const KuitSetup &setup) const
1399{
1400 OpenEl oel;
1401 oel.name = xml.name().toString().toLower();
1402
1403 // Collect attribute names and values, and format attribute string.
1404 QStringList attribNames;
1405 QStringList attribValues;
1406 const auto listAttributes = xml.attributes();
1407 attribNames.reserve(listAttributes.size());
1408 attribValues.reserve(listAttributes.size());
1409 for (const QXmlStreamAttribute &xatt : listAttributes) {
1410 attribNames += xatt.name().toString().toLower();
1411 attribValues += xatt.value().toString();
1412 QChar qc = attribValues.last().indexOf(QL1C('\'')) < 0 ? QL1C('\'') : QL1C('"');
1413 oel.attribStr += QL1C(' ') + attribNames.last() + QL1C('=') + qc + attribValues.last() + qc;
1414 }
1415
1416 auto tagIt = setup.d->knownTags.constFind(oel.name);
1417 if (tagIt != setup.d->knownTags.constEnd()) { // known KUIT element
1418 const KuitTag &tag = *tagIt;
1419 const KuitTag &etag = setup.d->knownTags.value(enclosingOel.name);
1420
1421 // If this element can be contained within enclosing element,
1422 // mark it proper, otherwise mark it for removal.
1423 if (tag.name.isEmpty() || tag.type == Kuit::PhraseTag || etag.type == Kuit::StructTag) {
1424 oel.handling = OpenEl::Proper;
1425 } else {
1426 oel.handling = OpenEl::Dropout;
1427 qCWarning(KI18N_KUIT)
1428 << QStringLiteral("Structuring tag ('%1') cannot be subtag of phrase tag ('%2') in message {%3}.").arg(tag.name, etag.name, shorten(text));
1429 }
1430
1431 // Resolve attributes and compute attribute set key.
1432 QSet<QString> attset;
1433 for (int i = 0; i < attribNames.size(); ++i) {
1434 QString att = attribNames[i];
1435 if (tag.knownAttribs.contains(att)) {
1436 attset << att;
1437 oel.attributes[att] = attribValues[i];
1438 } else {
1439 qCWarning(KI18N_KUIT) << QStringLiteral("Attribute '%1' not defined for tag '%2' in message {%3}.").arg(att, tag.name, shorten(text));
1440 }
1441 }
1442
1443 // Continue tag path.
1444 oel.tagPath = enclosingOel.tagPath;
1445 oel.tagPath.prepend(enclosingOel.name);
1446
1447 } else { // unknown element, leave it in verbatim
1448 oel.handling = OpenEl::Ignored;
1449 qCWarning(KI18N_KUIT) << QStringLiteral("Tag '%1' is not defined in message {%2}.").arg(oel.name, shorten(text));
1450 }
1451
1452 return oel;
1453}
1454
1455QString KuitFormatterPrivate::formatSubText(const QString &ptext, const OpenEl &oel, Kuit::VisualFormat format, const KuitSetup &setup) const
1456{
1457 if (oel.handling == OpenEl::Proper) {
1458 const KuitTag &tag = setup.d->knownTags.value(oel.name);
1459 QString ftext = tag.format(languageAsList, oel.attributes, oel.formattedText, oel.tagPath, format);
1460
1461 // Handle leading newlines, if this is not start of the text
1462 // (ptext is the preceding text).
1463 if (!ptext.isEmpty() && tag.leadingNewlines > 0) {
1464 // Count number of present newlines.
1465 int pnumle;
1466 int pnumtr;
1467 int fnumle;
1468 int fnumtr;
1469 countWrappingNewlines(ptext, pnumle, pnumtr);
1470 countWrappingNewlines(ftext, fnumle, fnumtr);
1471 // Number of leading newlines already present.
1472 int numle = pnumtr + fnumle;
1473 // The required extra newlines.
1474 QString strle;
1475 if (numle < tag.leadingNewlines) {
1476 strle = QString(tag.leadingNewlines - numle, QL1C('\n'));
1477 }
1478 ftext = strle + ftext;
1479 }
1480
1481 return ftext;
1482
1483 } else if (oel.handling == OpenEl::Ignored) {
1484 return QL1C('<') + oel.name + oel.attribStr + QL1C('>') + oel.formattedText + QSL("</") + oel.name + QL1C('>');
1485
1486 } else { // oel.handling == OpenEl::Dropout
1487 return oel.formattedText;
1488 }
1489}
1490
1491void KuitFormatterPrivate::countWrappingNewlines(const QString &text, int &numle, int &numtr)
1492{
1493 int len = text.length();
1494 // Number of newlines at start of text.
1495 numle = 0;
1496 while (numle < len && text[numle] == QL1C('\n')) {
1497 ++numle;
1498 }
1499 // Number of newlines at end of text.
1500 numtr = 0;
1501 while (numtr < len && text[len - numtr - 1] == QL1C('\n')) {
1502 ++numtr;
1503 }
1504}
1505
1506QString KuitFormatterPrivate::finalizeVisualText(const QString &text_, Kuit::VisualFormat format) const
1507{
1508 KuitStaticData *s = staticData();
1509
1510 QString text = text_;
1511
1512 // Resolve XML entities.
1513 if (format != Kuit::RichText) {
1514 // regex is (see s_entitySubRx var): (&([a-z]+|#[0-9]+|#x[0-9a-fA-F]+);)
1515 static const QRegularExpression entRx(QLatin1String("(&(") + QLatin1String(s_entitySubRx) + QLatin1String(");)"));
1517 QString plain;
1518 while ((match = entRx.match(text)).hasMatch()) {
1519 plain.append(QStringView(text).mid(0, match.capturedStart(0)));
1520 text.remove(0, match.capturedEnd(0));
1521 const QString ent = match.captured(2);
1522 if (ent.startsWith(QL1C('#'))) { // numeric character entity
1523 bool ok;
1524 QStringView entView(ent);
1525 const QChar c = ent.at(1) == QL1C('x') ? QChar(entView.mid(2).toInt(&ok, 16)) : QChar(entView.mid(1).toInt(&ok, 10));
1526 if (ok) {
1527 plain.append(c);
1528 } else { // unknown Unicode point, leave as is
1529 plain.append(match.capturedView(0));
1530 }
1531 } else if (s->xmlEntities.contains(ent)) { // known entity
1532 plain.append(s->xmlEntities[ent]);
1533 } else { // unknown entity, just leave as is
1534 plain.append(match.capturedView(0));
1535 }
1536 }
1537 plain.append(text);
1538 text = plain;
1539 }
1540
1541 // Add top tag.
1542 if (format == Kuit::RichText) {
1543 text = QLatin1String("<html>") + text + QLatin1String("</html>");
1544 }
1545
1546 return text;
1547}
1548
1549QString KuitFormatterPrivate::salvageMarkup(const QString &text_, Kuit::VisualFormat format, const KuitSetup &setup) const
1550{
1551 QString text = text_;
1552 QString ntext;
1553
1554 // Resolve tags simple-mindedly.
1555
1556 // - tags with content
1557 static const QRegularExpression wrapRx(QStringLiteral("(<\\s*(\\w+)\\b([^>]*)>)(.*)(<\\s*/\\s*\\2\\s*>)"), QRegularExpression::InvertedGreedinessOption);
1558 QRegularExpressionMatchIterator iter = wrapRx.globalMatch(text);
1560 int pos = 0;
1561 while (iter.hasNext()) {
1562 match = iter.next();
1563 ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos);
1564 const QString tagname = match.captured(2).toLower();
1565 const QString content = salvageMarkup(match.captured(4), format, setup);
1566 auto tagIt = setup.d->knownTags.constFind(tagname);
1567 if (tagIt != setup.d->knownTags.constEnd()) {
1568 const KuitTag &tag = *tagIt;
1569 QHash<QString, QString> attributes;
1570 // TODO: Do not ignore attributes (in match.captured(3)).
1571 ntext += tag.format(languageAsList, attributes, content, QStringList(), format);
1572 } else {
1573 ntext += match.captured(1) + content + match.captured(5);
1574 }
1575 pos = match.capturedEnd(0);
1576 }
1577 // get the remaining part after the last match in "text"
1578 ntext += QStringView(text).mid(pos);
1579 text = ntext;
1580
1581 // - tags without content
1582 static const QRegularExpression nowrRx(QStringLiteral("<\\s*(\\w+)\\b([^>]*)/\\s*>"), QRegularExpression::InvertedGreedinessOption);
1583 iter = nowrRx.globalMatch(text);
1584 pos = 0;
1585 ntext.clear();
1586 while (iter.hasNext()) {
1587 match = iter.next();
1588 ntext += QStringView(text).mid(pos, match.capturedStart(0) - pos);
1589 const QString tagname = match.captured(1).toLower();
1590 auto tagIt = setup.d->knownTags.constFind(tagname);
1591 if (tagIt != setup.d->knownTags.constEnd()) {
1592 const KuitTag &tag = *tagIt;
1593 ntext += tag.format(languageAsList, QHash<QString, QString>(), QString(), QStringList(), format);
1594 } else {
1595 ntext += match.captured(0);
1596 }
1597 pos = match.capturedEnd(0);
1598 }
1599 // get the remaining part after the last match in "text"
1600 ntext += QStringView(text).mid(pos);
1601 text = ntext;
1602
1603 // Add top tag.
1604 if (format == Kuit::RichText) {
1605 text = QStringLiteral("<html>") + text + QStringLiteral("</html>");
1606 }
1607
1608 return text;
1609}
1610
1611KuitFormatter::KuitFormatter(const QString &language)
1612 : d(new KuitFormatterPrivate(language))
1613{
1614}
1615
1616KuitFormatter::~KuitFormatter()
1617{
1618 delete d;
1619}
1620
1621QString KuitFormatter::format(const QByteArray &domain, const QString &context, const QString &text, Kuit::VisualFormat format) const
1622{
1623 return d->format(domain, context, text, format);
1624}
Lazy-initialized variant of KLocalizedString.
constexpr const char * untranslatedText() const
Returns the raw untranslated text as passed to kli18n*.
Class for producing and handling localized messages.
QString toString() const
Finalize the translation.
KLocalizedString ignoreMarkup() const
Do not resolve KUIT markup.
bool isEmpty() const
Check whether the message is empty.
KLocalizedString subs(int a, int fieldWidth=0, int base=10, QChar fillChar=QLatin1Char(' ')) const
Substitute an int argument into the message.
KLocalizedString relaxSubs() const
Relax matching between placeholders and arguments.
Class for modifying KUIT markup in a given domain.
Definition kuitsetup.h:102
~KuitSetup()
Destructor.
void setTagPattern(const QString &tagName, const QStringList &attribNames, Kuit::VisualFormat format, const KLocalizedString &pattern, Kuit::TagFormatter formatter=nullptr, int leadingNewlines=0)
Set the formatting string for a tag with attributes combination.
void setFormatForMarker(const QString &marker, Kuit::VisualFormat format)
Set the default visual format for a given UI marker.
void setTagClass(const QString &tagName, Kuit::TagClass aClass)
Set the KUIT class of the tag.
Type type(const QSqlDatabase &db)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString name(StandardAction id)
Global constants and functions related to KUIT markup.
TagClass
Classification of KUIT tags.
Definition kuitsetup.h:52
@ StructTag
Tags splitting text into paragraph-level blocks.
Definition kuitsetup.h:60
@ PhraseTag
Tags wrapping text inserted into running text.
Definition kuitsetup.h:56
QString(* TagFormatter)(const QStringList &languages, const QString &tagName, const QHash< QString, QString > &attributes, const QString &text, const QStringList &tagPath, Kuit::VisualFormat format)
Functions accepted by tag formatting functions.
Definition kuitsetup.h:74
VisualFormat
Visual formats into which KUIT markup can be resolved.
Definition kuitsetup.h:26
@ TermText
Terminal escape sequences.
Definition kuitsetup.h:46
@ UndefinedFormat
Visual format not defined.
Definition kuitsetup.h:34
@ RichText
Qt rich text (HTML subset).
Definition kuitsetup.h:42
@ PlainText
Plain text.
Definition kuitsetup.h:38
KI18N_EXPORT KuitSetup & setupForDomain(const QByteArray &domain)
Get hold of the KUIT setup object for a given domain.
QString toNativeSeparators(const QString &pathName)
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
bool contains(const Key &key) const const
iterator end()
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
QList< Key > keys() const const
T value(const Key &key) const const
void append(QList< T > &&value)
iterator begin()
iterator end()
bool isEmpty() const const
T & last()
qsizetype removeAll(const AT &t)
void reserve(qsizetype size)
qsizetype size() const const
T value(qsizetype i) const const
QRegularExpressionMatch next()
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
void push(const T &t)
T & top()
QString & append(QChar ch)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
void clear()
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
void reserve(qsizetype size)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toLower() const const
QString trimmed() const const
void truncate(qsizetype position)
QString join(QChar separator) const const
QStringView left(qsizetype length) const const
QStringView mid(qsizetype start, qsizetype length) const const
QString toString() const const
SkipEmptyParts
RichText
QXmlStreamAttributes attributes() const const
QStringView name() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:05:34 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.