File indexing completed on 2024-12-22 04:48:10
0001 /* 0002 SPDX-License-Identifier: GPL-2.0-or-later 0003 SPDX-FileCopyrightText: 2023 Louis Schul <schul9louis@gmail.com> 0004 */ 0005 0006 // CREDIT TO ORIGINAL IDEA: https://marked.js.org/ 0007 #include "inlineLexer.h" 0008 // #include <QDebug> 0009 0010 #include <QDir> 0011 #include <QMap> 0012 #include <QRandomGenerator> 0013 0014 #include "kleverconfig.h" 0015 #include "logic/plugins/emoji/emojiModel.h" 0016 #include "parser.h" 0017 #include "renderer.h" 0018 0019 InlineLexer::InlineLexer(Parser *parser) 0020 : m_parser(parser) 0021 { 0022 } 0023 0024 QString InlineLexer::output(QString &src, bool useInlineText) 0025 { 0026 static const QString emptyStr = QLatin1String(); 0027 QString out = emptyStr, text, href, title, cap0, cap1, cap2, cap3, cap4, outputed; 0028 QMap<QString, QString> linkInfo; 0029 QRegularExpressionMatch cap, secondCap; 0030 0031 static const PluginHelper *pluginHelper = m_parser->getPluginHelper(); 0032 static NoteMapperParserUtils *mapperParserUtils = pluginHelper->getMapperParserUtils(); 0033 0034 while (!src.isEmpty()) { 0035 cap = inline_escape.match(src); 0036 if (cap.hasMatch()) { 0037 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0038 0039 out += cap.captured(1); 0040 continue; 0041 } 0042 0043 cap = inline_autolink.match(src); 0044 if (cap.hasMatch()) { 0045 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0046 0047 if (cap.captured(2) == QStringLiteral("@")) { 0048 cap1 = cap.captured(1); 0049 QString mangled = mangle(cap1); 0050 text = Renderer::escape(mangled, false); 0051 href = QStringLiteral("mailto:") + text; 0052 } else { 0053 cap1 = cap.captured(1); 0054 text = Renderer::escape(cap1, false); 0055 href = text; 0056 } 0057 title = emptyStr; 0058 0059 out += Renderer::link(href, title, text); 0060 continue; 0061 } 0062 0063 // url (gfm) 0064 cap = inline_url.match(src); 0065 if (!m_inLink && cap.hasMatch()) { 0066 cap0 = inline_backPedal.match(cap.captured(0)).captured(0); 0067 src.replace(src.indexOf(cap0), cap.capturedLength(), emptyStr); 0068 0069 if (cap.captured(2) == QStringLiteral("@")) { 0070 text = Renderer::escape(cap0, false); 0071 href = QStringLiteral("mailto:") + text; 0072 } else { 0073 text = Renderer::escape(cap0, false); 0074 if (cap.captured(1) == QStringLiteral("www.")) { 0075 href = QStringLiteral("http://") + text; 0076 } else { 0077 href = text; 0078 } 0079 } 0080 title = emptyStr; 0081 0082 out += Renderer::link(href, title, text); 0083 continue; 0084 } 0085 0086 // tag 0087 cap = inline_tag.match(src); 0088 if (cap.hasMatch()) { 0089 static const QRegularExpression aTagOpenReg = QRegularExpression(QStringLiteral("^<a "), QRegularExpression::CaseInsensitiveOption); 0090 const bool hasOpeningLink = aTagOpenReg.match(cap.captured(0)).hasMatch(); 0091 static const QRegularExpression aTagCloseRag = QRegularExpression(QStringLiteral("^<\\/a>"), QRegularExpression::CaseInsensitiveOption); 0092 const bool hasClosingLink = aTagCloseRag.match(cap.captured(0)).hasMatch(); 0093 0094 if (!m_inLink && hasOpeningLink) { 0095 m_inLink = true; 0096 } else if (m_inLink && hasClosingLink) { 0097 m_inLink = false; 0098 } 0099 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0100 0101 out += cap.captured(0); 0102 continue; 0103 } 0104 0105 // link 0106 cap = inline_link.match(src); 0107 if (cap.hasMatch()) { 0108 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0109 0110 m_inLink = true; 0111 href = cap.captured(2); 0112 0113 const int end = cap.captured(3).length() - 2; 0114 title = !cap.captured(3).isEmpty() ? cap.captured(3).mid(1, end) : emptyStr; 0115 0116 href = href.trimmed(); 0117 static const QRegularExpression tagReg = QRegularExpression(QStringLiteral("^<([\\s\\S]*)>$")); 0118 const QRegularExpressionMatch tagMatch = tagReg.match(href); 0119 href = href.replace(tagMatch.capturedStart(), tagMatch.capturedLength(), tagMatch.captured(1)); 0120 linkInfo = {{QStringLiteral("href"), escapes(href)}, {QStringLiteral("title"), escapes(title)}}; 0121 0122 out += outputLink(cap, linkInfo, useInlineText); 0123 m_inLink = false; 0124 continue; 0125 } 0126 0127 // wikilink 0128 if (KleverConfig::noteMapEnabled()) { 0129 const static QRegularExpression inline_wikilink = 0130 QRegularExpression(QStringLiteral("\\[\\[([^:\\]\\|\\r\\n]*)(:)?([^:\\]\\|\\r\\n]*)(\\|)?([^:\\]\\|\\r\\n]*)\\]\\]")); 0131 cap = inline_wikilink.match(src); 0132 if (cap.hasMatch()) { 0133 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0134 if (!cap.captured(1).trimmed().isEmpty()) { 0135 href = cap.captured(1).trimmed(); 0136 const QPair<QString, bool> sanitizedHref = mapperParserUtils->sanitizePath(href); 0137 0138 cap3 = cap.captured(3).trimmed(); 0139 0140 const bool hasPipe = !cap.captured(4).isEmpty(); 0141 0142 const QString potentitalTitle = cap.captured(5).trimmed(); 0143 title = hasPipe && !potentitalTitle.isEmpty() ? potentitalTitle : sanitizedHref.first.split(QStringLiteral("/")).last(); 0144 0145 if (sanitizedHref.second) { 0146 mapperParserUtils->addToLinkedNoteInfos({sanitizedHref.first, cap3, title}); 0147 // This hopefuly, is enough to separate the 2 without collinding with user input 0148 QString fullLink = sanitizedHref.first + QStringLiteral("@HEADER@") + cap3; // <Note path>@HEADER@<header ref> 0149 out += Renderer::wikilink(fullLink, title, title); 0150 continue; 0151 } 0152 } 0153 // Not a note path 0154 out += Renderer::paragraph(cap.captured(0)); 0155 continue; 0156 } 0157 } 0158 0159 // reflink, nolink 0160 const QRegularExpressionMatch tempMatch = inline_reflink.match(src); 0161 if (tempMatch.hasMatch()) 0162 cap = tempMatch; 0163 else 0164 cap = inline_nolink.match(src); 0165 0166 if (cap.hasMatch()) { 0167 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0168 0169 cap2 = cap.captured(2); 0170 static const QRegularExpression whiteSpaceReg = QRegularExpression(QStringLiteral("\\s+")); 0171 cap1 = cap.captured(1).replace(whiteSpaceReg, QStringLiteral(" ")); 0172 0173 const QString linkId = !cap2.isEmpty() ? cap2 : cap1; 0174 0175 linkInfo = m_parser->links[linkId.toLower()]; 0176 if (linkInfo.isEmpty() || linkInfo[QStringLiteral("href")].isEmpty()) { 0177 out += cap.captured(0).at(0); 0178 src = cap.captured(0).mid(1) + src; 0179 continue; 0180 } 0181 0182 m_inLink = true; 0183 out += outputLink(cap, linkInfo, useInlineText); 0184 m_inLink = false; 0185 continue; 0186 } 0187 0188 // strong 0189 cap = inline_strong.match(src); 0190 if (cap.hasMatch()) { 0191 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0192 0193 cap4 = cap.captured(4); 0194 cap3 = cap.captured(3); 0195 cap2 = cap.captured(2); 0196 cap1 = cap.captured(1); 0197 0198 QString toOutput; 0199 if (!cap4.isEmpty()) 0200 toOutput = cap4; 0201 else if (!cap3.isEmpty()) 0202 toOutput = cap3; 0203 else if (!cap2.isEmpty()) 0204 toOutput = cap2; 0205 else 0206 toOutput = cap1; 0207 0208 outputed = output(toOutput); 0209 0210 out += Renderer::strong(outputed); 0211 continue; 0212 } 0213 0214 // em 0215 cap = inline_em.match(src); 0216 if (cap.hasMatch()) { 0217 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0218 0219 const QString cap6 = cap.captured(6); 0220 const QString cap5 = cap.captured(5); 0221 cap4 = cap.captured(4); 0222 cap3 = cap.captured(3); 0223 cap2 = cap.captured(2); 0224 cap1 = cap.captured(1); 0225 0226 QString toOutput; 0227 if (!cap6.isEmpty()) 0228 toOutput = cap6; 0229 else if (!cap5.isEmpty()) 0230 toOutput = cap5; 0231 else if (!cap4.isEmpty()) 0232 toOutput = cap4; 0233 else if (!cap3.isEmpty()) 0234 toOutput = cap3; 0235 else if (!cap2.isEmpty()) 0236 toOutput = cap2; 0237 else 0238 toOutput = cap1; 0239 0240 outputed = output(toOutput); 0241 0242 out += Renderer::em(outputed); 0243 continue; 0244 } 0245 0246 // code 0247 cap = inline_code.match(src); 0248 if (cap.hasMatch()) { 0249 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0250 0251 QString capTrimmed = cap.captured(2).trimmed(); 0252 const QString escaped = Renderer::escape(capTrimmed, true); 0253 0254 out += Renderer::codeSpan(escaped); 0255 continue; 0256 } 0257 0258 // br 0259 cap = inline_br.match(src); 0260 if (cap.hasMatch()) { 0261 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0262 0263 if (!useInlineText) 0264 out += Renderer::br(); 0265 continue; 0266 } 0267 0268 // del (gfm) 0269 cap = inline_del.match(src); 0270 if (cap.hasMatch()) { 0271 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0272 0273 cap1 = cap.captured(1); 0274 outputed = output(cap1); 0275 0276 out += Renderer::del(outputed); 0277 0278 continue; 0279 } 0280 0281 cap = inline_highlight.match(src); 0282 if (cap.hasMatch()) { 0283 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0284 0285 cap1 = cap.captured(1); 0286 outputed = output(cap1); 0287 0288 out += Renderer::mark(outputed); 0289 continue; 0290 } 0291 0292 // emoji 0293 if (KleverConfig::quickEmojiEnabled()) { 0294 static const auto emojiModel = &EmojiModel::instance(); 0295 static const QRegularExpression inline_emoji = QRegularExpression(QStringLiteral("^:(?=\\S)([^:]*)(:?)([^:]*):")); 0296 static const QString defaultToneStr = QStringLiteral("default skin tone"); 0297 static const QSet<QString> possibleTones = { 0298 QStringLiteral("dark skin tone"), 0299 QStringLiteral("medium-dark skin tone"), 0300 QStringLiteral("medium skin tone"), 0301 QStringLiteral("medium-light skin tone"), 0302 QStringLiteral("light skin tone"), 0303 defaultToneStr, // To possibly overwrite the default in config 0304 }; 0305 0306 cap = inline_emoji.match(src); 0307 if (cap.hasMatch()) { 0308 cap0 = cap.captured(0); 0309 cap1 = cap.captured(1).trimmed(); 0310 cap3 = cap.captured(3).trimmed(); 0311 0312 QStringList variantInfo; 0313 0314 if (!cap3.isEmpty()) { 0315 variantInfo = cap3.split(QStringLiteral(",")); 0316 } 0317 0318 const QString configTone = KleverConfig::emojiTone(); 0319 QString tone = configTone == QStringLiteral("None") ? QLatin1String() : configTone; 0320 QString givenVariant; 0321 bool toneGiven = false; 0322 bool optionsFound = false; 0323 if (!variantInfo.isEmpty()) { 0324 // Ex: 0325 // "woman: dark skin tone, blond hair" 0326 // "woman: blond hair" 0327 const QString possibleTone = variantInfo[0].trimmed(); 0328 if (possibleTones.contains(possibleTone)) { 0329 tone = possibleTone; 0330 toneGiven = true; 0331 } else { 0332 givenVariant = possibleTone; 0333 } 0334 if (variantInfo.length() > 1) { 0335 givenVariant = variantInfo[1].trimmed(); 0336 } 0337 } 0338 const bool defaultToneGiven = tone == defaultToneStr; 0339 0340 QString uniEmoji; 0341 const QString searchTerm = givenVariant.isEmpty() ? cap1 : (cap1 + QStringLiteral(": ") + givenVariant); 0342 0343 if (!tone.isEmpty() && !defaultToneGiven) { 0344 const QVariantList tonedEmoji = emojiModel->tones(cap1); 0345 for (auto it = tonedEmoji.begin(); it != tonedEmoji.end(); it++) { 0346 const Emoji currentEmoji = it->value<Emoji>(); 0347 const QString emojiName = currentEmoji.shortName; 0348 if (emojiName.contains(tone)) { 0349 uniEmoji = currentEmoji.unicode; 0350 0351 if (givenVariant.isEmpty()) { // only looking for tone 0352 if (toneGiven) { 0353 optionsFound = true; 0354 } 0355 break; 0356 } 0357 // looking for tone + variant 0358 if (emojiName.endsWith(givenVariant)) { 0359 optionsFound = true; 0360 break; 0361 } 0362 } 0363 } 0364 } else { 0365 const QVariantList possibleEmojis = emojiModel->filterModelNoCustom(searchTerm); 0366 for (auto it = possibleEmojis.begin(); it != possibleEmojis.end(); it++) { 0367 const Emoji currentEmoji = it->value<Emoji>(); 0368 if (currentEmoji.shortName == searchTerm) { 0369 uniEmoji = currentEmoji.unicode; 0370 if (!givenVariant.isEmpty() || defaultToneGiven) { 0371 optionsFound = true; 0372 } 0373 break; 0374 } 0375 } 0376 } 0377 0378 if (optionsFound) { 0379 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0380 } else { 0381 static const QString surroundingStr = QStringLiteral("::"); 0382 src.replace(cap.capturedStart(), cap1.length() + surroundingStr.length(), emptyStr); 0383 } 0384 0385 outputed = uniEmoji.isEmpty() ? output(cap2) : uniEmoji; 0386 0387 out += Renderer::text(outputed); 0388 continue; 0389 } 0390 } 0391 0392 // text 0393 cap = inline_text.match(src); 0394 if (cap.hasMatch()) { 0395 src.replace(cap.capturedStart(), cap.capturedLength(), emptyStr); 0396 0397 cap0 = cap.captured(0); 0398 const QString escaped = Renderer::escape(cap0, false); 0399 0400 out += (useInlineText) ? escaped : Renderer::text(escaped); 0401 continue; 0402 } 0403 0404 if (!src.isEmpty()) { 0405 qFatal("Infinite loop on byte: %d", src[0].unicode()); 0406 } 0407 } 0408 0409 return out; 0410 }; 0411 0412 QString InlineLexer::mangle(const QString &text) const 0413 { 0414 QString out = QLatin1String(); 0415 const int l = text.length(); 0416 0417 for (int i = 0; i < l; i++) { 0418 const QChar ch = text.at(i); 0419 if (QRandomGenerator::global()->generate() % 2 == 0) { 0420 out += QStringLiteral("x") + QString::number(ch.unicode(), 16); 0421 } else { 0422 out += QStringLiteral("&#") + QString::number(ch.unicode()) + QStringLiteral(";"); 0423 } 0424 } 0425 0426 return out; 0427 } 0428 0429 QString InlineLexer::outputLink(QRegularExpressionMatch &cap, QMap<QString, QString> linkInfo, bool useInlineText) 0430 { 0431 QString href = linkInfo[QStringLiteral("href")]; 0432 QString title = linkInfo[QStringLiteral("title")]; 0433 title = !title.isEmpty() ? Renderer::escape(title, false) : QLatin1String(); 0434 0435 // KLEVERNOTES ADDED THOSE LINES 0436 // ====== 0437 if (href.startsWith(QStringLiteral("./"))) 0438 href = m_parser->getNotePath() + href.mid(1); 0439 if (href.startsWith(QStringLiteral("~"))) 0440 href = QDir::homePath() + href.mid(1); 0441 if (!(href.startsWith(QStringLiteral("http")) || href.startsWith(QStringLiteral("//")) || href.startsWith(QStringLiteral("qrc:")))) 0442 href = QStringLiteral("file:") + href; 0443 // ====== 0444 QString out; 0445 QString cap1 = cap.captured(1); 0446 if (cap.captured(0).at(0) != QChar::fromLatin1('!')) { 0447 const QString outputed = output(cap1); 0448 out = Renderer::link(href, title, outputed); 0449 } else { 0450 const QString escaped = Renderer::escape(cap1, false); 0451 out = (useInlineText) ? escaped : Renderer::image(href, title, escaped); 0452 } 0453 0454 return out; 0455 } 0456 0457 QString InlineLexer::escapes(QString &text) const 0458 { 0459 const static QRegularExpression escapesReg(QStringLiteral("\\\\([!\"#$%&'()*+,\\-.\\/:;<=>?@\\[\\]\\^_`{|}~])")); 0460 0461 return !text.isEmpty() ? text.replace(escapesReg, QStringLiteral("\\1")) : text; 0462 }