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