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