| 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 | |