File indexing completed on 2024-04-21 04:38:10

0001 /*
0002     SPDX-FileCopyrightText: 2018 Anton Anikin <anton@anikin.xyz>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "utils.h"
0008 
0009 // KDevPlatform
0010 #include <interfaces/icore.h>
0011 #include <interfaces/iproject.h>
0012 #include <interfaces/iprojectcontroller.h>
0013 // KF
0014 #include <KLocalizedString>
0015 // Qt
0016 #include <QFile>
0017 #include <QRegularExpression>
0018 #include <QVector>
0019 
0020 namespace Clazy
0021 {
0022 
0023 QString prettyPathName(const QUrl& path)
0024 {
0025     return KDevelop::ICore::self()->projectController()->prettyFileName(path, KDevelop::IProjectController::FormatPlain);
0026 }
0027 
0028 // Very simple Markdown parser/converter. Does not provide full Markdown language support and
0029 // was tested only with Clazy documentation.
0030 class MarkdownConverter
0031 {
0032 public:
0033     MarkdownConverter()
0034     {
0035         tagStart.resize(STATE_COUNT);
0036         tagEnd.resize(STATE_COUNT);
0037 
0038         tagStart[EMPTY].clear();
0039         tagEnd  [EMPTY].clear();
0040 
0041         tagStart[HEADING] = QStringLiteral("<b>");
0042         tagEnd  [HEADING] = QStringLiteral("</b>");
0043 
0044         tagStart[PARAGRAPH] = QStringLiteral("<p>");
0045         tagEnd  [PARAGRAPH] = QStringLiteral("</p>");
0046 
0047         tagStart[PREFORMATTED] = QStringLiteral("<pre>");
0048         tagEnd  [PREFORMATTED] = QStringLiteral("</pre>");
0049 
0050         tagStart[LIST] = QStringLiteral("<ul><li>");
0051         tagEnd  [LIST] = QStringLiteral("</li></ul>");
0052     }
0053 
0054     ~MarkdownConverter() = default;
0055 
0056     QString toHtml(const QString& markdown)
0057     {
0058         const QRegularExpression hRE(QStringLiteral("(#+) (.+)"));
0059         QRegularExpressionMatch match;
0060 
0061         state = EMPTY;
0062         html.clear();
0063         html += QStringLiteral("<html>");
0064 
0065         const auto lines = markdown.split(QLatin1Char('\n'));
0066         for (auto line : lines) {
0067             if (line.isEmpty()) {
0068                 setState(EMPTY);
0069                 continue;
0070             }
0071 
0072             if (line.startsWith(QLatin1Char('#'))) {
0073                 auto match = hRE.match(line);
0074                 if (match.hasMatch()) {
0075                     setState(HEADING);
0076                     html += match.captured(2);
0077                     setState(EMPTY);
0078                     if (match.capturedRef(1).size() == 1) {
0079                         html += QStringLiteral("<hr>");
0080                     }
0081                 }
0082                 continue;
0083             }
0084 
0085             if (line.startsWith(QLatin1String("```"))) {
0086                 setState((state == PREFORMATTED) ? EMPTY : PREFORMATTED);
0087                 continue;
0088             }
0089 
0090             if (line.startsWith(QLatin1String("    "))) {
0091                 if (state == EMPTY) {
0092                     setState(PREFORMATTED);
0093                 }
0094             } else if (
0095                 line.startsWith(QLatin1String("- ")) ||
0096                 line.startsWith(QLatin1String("* "))) {
0097                 // force close and reopen list - this fixes cases when we don't have
0098                 // separator line between items
0099                 setState(EMPTY);
0100                 setState(LIST);
0101                 line.remove(0, 2);
0102             }
0103 
0104             if (state == EMPTY) {
0105                 setState(PARAGRAPH);
0106             }
0107 
0108             processLine(line);
0109         }
0110         setState(EMPTY);
0111 
0112         html += QStringLiteral("</html>");
0113         return html.join(QLatin1Char('\n'));
0114     }
0115 
0116 private:
0117     enum STATE {
0118         EMPTY,
0119         HEADING,
0120         PARAGRAPH,
0121         PREFORMATTED,
0122         LIST,
0123 
0124         STATE_COUNT
0125     };
0126 
0127     void setState(int newState)
0128     {
0129         if (state == newState) {
0130             return;
0131         }
0132 
0133         if (state != EMPTY) {
0134             html += tagEnd[state];
0135         }
0136 
0137         if (newState != EMPTY) {
0138             html += tagStart[newState];
0139         }
0140 
0141         state = newState;
0142     }
0143 
0144     void processLine(QString& line)
0145     {
0146         static const QRegularExpression ttRE(QStringLiteral("`([^`]+)`"));
0147         static const QRegularExpression bdRE(QStringLiteral("\\*\\*([^\\*]+)\\*\\*"));
0148         static const QRegularExpression itRE(QStringLiteral("[^\\*]\\*([^\\*]+)\\*[^\\*]"));
0149 
0150         static auto applyRE = [](const QRegularExpression& re, QString& line, const QString& tag) {
0151             auto i = re.globalMatch(line);
0152             while (i.hasNext()) {
0153                 auto match = i.next();
0154                 line.replace(match.captured(0), QStringLiteral("<%1>%2</%1>").arg(tag, match.captured(1)));
0155             }
0156         };
0157 
0158         if (state != PREFORMATTED) {
0159             line.replace(QLatin1Char('&'), QLatin1String("&amp;"));
0160             line.replace(QLatin1Char('<'), QLatin1String("&lt;"));
0161             line.replace(QLatin1Char('>'), QLatin1String("&gt;"));
0162 
0163             line.replace(QLatin1Char('\"'), QLatin1String("&quot;"));
0164             line.replace(QLatin1Char('\''), QLatin1String("&#39;"));
0165 
0166             applyRE(ttRE, line, QStringLiteral("tt"));
0167             applyRE(bdRE, line, QStringLiteral("b"));
0168             applyRE(itRE, line, QStringLiteral("i"));
0169         }
0170 
0171         html += line;
0172     }
0173 
0174 private:
0175     int state;
0176     QVector<QString> tagStart;
0177     QVector<QString> tagEnd;
0178     QStringList html;
0179 };
0180 
0181 QString markdown2html(const QByteArray& markdown)
0182 {
0183     MarkdownConverter converter;
0184     return converter.toHtml(QString::fromUtf8(markdown));
0185 }
0186 
0187 }