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 }