File indexing completed on 2024-12-22 04:48:18

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2023 Louis Schul <schul9louis@gmail.com>
0004 */
0005 
0006 #include "noteMapper.h"
0007 #include "kleverconfig.h"
0008 #include "logic/documentHandler.h"
0009 #include "noteMapperUtils.h"
0010 // #include <QDebug>
0011 #include <QJsonArray>
0012 
0013 LinkedNoteItem::LinkedNoteItem(const QString &path,
0014                                const QString &exists,
0015                                const QString &header,
0016                                const bool headerExists,
0017                                const int headerLevel,
0018                                const QString &title)
0019     : m_exists(exists)
0020     , m_header(header)
0021     , m_headerExists(headerExists)
0022     , m_headerLevel(headerLevel)
0023     , m_title(title)
0024 {
0025     updatePath(path);
0026 }
0027 
0028 QVariant LinkedNoteItem::data(int role) const
0029 {
0030     switch (role) {
0031     case NoteMapper::PathRole:
0032         return m_path;
0033 
0034     case NoteMapper::DisplayedPathRole:
0035         return m_displayPath;
0036 
0037     case NoteMapper::ExistsRole:
0038         return m_exists;
0039 
0040     case NoteMapper::HeaderRole:
0041         return m_header;
0042 
0043     case NoteMapper::HeaderExistsRole:
0044         return m_headerExists;
0045 
0046     case NoteMapper::HeaderLevelRole:
0047         return m_headerLevel;
0048 
0049     case NoteMapper::TitleRole:
0050         return m_title;
0051     }
0052     // default Q_UNREACHABLE would crash the app when doing the following :
0053     // - be in a note
0054     // - click on a group/category
0055     // - go back to the same note
0056     // => repeater goes crazy and send a role = 0, 2 times for each entry
0057     // before going back to normal
0058     return 0;
0059 };
0060 
0061 void LinkedNoteItem::updatePath(const QString &path)
0062 {
0063     m_path = path;
0064     setDisplayPath(path);
0065 }
0066 
0067 void LinkedNoteItem::setDisplayPath(const QString &path)
0068 {
0069     auto newPath = path;
0070     newPath.replace(QStringLiteral(".BaseCategory"), KleverConfig::defaultCategoryDisplayNameValue()).remove(QStringLiteral(".BaseGroup/"));
0071     m_displayPath = newPath;
0072 }
0073 
0074 void LinkedNoteItem::updateExists(const QString &exists)
0075 {
0076     m_exists = exists;
0077 }
0078 
0079 void LinkedNoteItem::updateHeaderExists(const bool exists)
0080 {
0081     m_headerExists = exists;
0082 }
0083 
0084 NoteMapper::NoteMapper(QObject *parent)
0085     : QAbstractItemModel(parent)
0086 {
0087     const QString mapPath = KleverConfig::storagePath() + QStringLiteral("/notesMap.json");
0088     m_savedMap = NoteMapperUtils::convertSavedMap(DocumentHandler::getJson(mapPath));
0089 }
0090 
0091 void NoteMapper::saveMap() const
0092 {
0093     const QJsonObject json = QJsonObject::fromVariantMap(m_existsMap);
0094     const QString savingPath = KleverConfig::storagePath() + QStringLiteral("/notesMap.json");
0095     DocumentHandler::saveJson(json, savingPath);
0096 }
0097 
0098 QModelIndex NoteMapper::index(int row, int column, const QModelIndex &parent) const
0099 {
0100     Q_UNUSED(parent)
0101     return createIndex(row, column, static_cast<LinkedNoteItem *>(m_list.at(row).get()));
0102 }
0103 
0104 QHash<int, QByteArray> NoteMapper::roleNames() const
0105 {
0106     return {
0107         {DisplayedPathRole, "displayedPath"},
0108         {PathRole, "notePath"},
0109         {ExistsRole, "exists"},
0110         {HeaderRole, "header"},
0111         {HeaderExistsRole, "headerExists"},
0112         {HeaderLevelRole, "headerLevel"},
0113         {TitleRole, "title"},
0114     };
0115 }
0116 
0117 QModelIndex NoteMapper::parent(const QModelIndex &index) const
0118 {
0119     Q_UNUSED(index)
0120     return {};
0121 }
0122 
0123 int NoteMapper::rowCount(const QModelIndex &parent) const
0124 {
0125     Q_UNUSED(parent)
0126     return m_list.size();
0127 }
0128 
0129 int NoteMapper::columnCount(const QModelIndex &parent) const
0130 {
0131     Q_UNUSED(parent)
0132     return 1;
0133 }
0134 
0135 QVariant NoteMapper::data(const QModelIndex &index, int role) const
0136 {
0137     if (!index.isValid()) {
0138         return {};
0139     }
0140     const LinkedNoteItem *item = static_cast<LinkedNoteItem *>(index.internalPointer());
0141     return item->data(role);
0142 }
0143 
0144 void NoteMapper::clear()
0145 {
0146     beginResetModel();
0147     m_list.clear();
0148     endResetModel();
0149 }
0150 
0151 QVariantMap NoteMapper::getPathInfo(const QString &path) const
0152 {
0153     static const QVariantMap emptyMap;
0154     return m_existsMap.contains(path) ? m_existsMap[path].toMap() : emptyMap;
0155 }
0156 
0157 void NoteMapper::addRow(const QStringList &infos)
0158 {
0159     bool headerExists = false;
0160     const QString path = infos.at(0);
0161     const QString header = infos.at(1);
0162     const QString title = infos.at(2);
0163 
0164     const QString cleanedHeader = NoteMapperUtils::cleanHeader(header); // The header is trimmed by default, see inlineLexer wikilink => captured(3).trimmed
0165     const int headerLevel = NoteMapperUtils::headerLevel(cleanedHeader);
0166     const QString headerText = NoteMapperUtils::headerText(cleanedHeader);
0167 
0168     QString exists = QStringLiteral("No"); // Not a bool because used for KSortFilterProxyModel filterString
0169     if (m_treeViewPaths.contains(path)) {
0170         exists = QStringLiteral("Yes");
0171 
0172         static const QString headersStr = QStringLiteral("headers");
0173         static const QString entirelyCheckStr = QStringLiteral("entirelyCheck");
0174         if (!headerText.isEmpty()) {
0175             const QVariantMap pathInfo = getPathInfo(path);
0176             const bool entirelyCheck = NoteMapperUtils::entirelyChecked(pathInfo);
0177             QStringList headers = NoteMapperUtils::getNoteHeaders(pathInfo);
0178             headerExists = headers.contains(cleanedHeader);
0179 
0180             if (!headerExists && !entirelyCheck) {
0181                 const QString filePath = KleverConfig::storagePath() + path + QStringLiteral("/note.md");
0182                 headerExists = DocumentHandler::checkForHeader(filePath, cleanedHeader);
0183                 if (headerExists) {
0184                     // We don't update the "entirelyCheck" value, only NoteMapper::updatePathInfo can do it
0185                     headers.append(cleanedHeader);
0186                     const QVariantMap newPathInfo = {{headersStr, QVariant(headers)}, {entirelyCheckStr, QVariant(entirelyCheck)}};
0187                     m_existsMap[path] = newPathInfo;
0188                 }
0189             }
0190         }
0191     }
0192 
0193     auto newRow = std::make_unique<LinkedNoteItem>(path, exists, headerText, headerExists, headerLevel, title); // making it a const prevent std::move
0194 
0195     beginInsertRows(QModelIndex(), rowCount(), rowCount());
0196     m_list.push_back(std::move(newRow));
0197     endInsertRows();
0198 }
0199 
0200 QVariantList NoteMapper::getCleanedHeaderAndLevel(const QString &header) const
0201 {
0202     const QString cleanedHeader = NoteMapperUtils::cleanHeader(header);
0203     const int headerLevel = NoteMapperUtils::headerLevel(cleanedHeader);
0204     const QString headerText = NoteMapperUtils::headerText(cleanedHeader);
0205 
0206     return {headerText, headerLevel};
0207 }
0208 
0209 QList<QVariantMap> NoteMapper::getNoteHeaders(const QString &notePath)
0210 {
0211     const QString cleanedPath = QString(notePath).remove(KleverConfig::storagePath());
0212     const QVariantMap pathInfo = getPathInfo(cleanedPath);
0213 
0214     if (NoteMapperUtils::entirelyChecked(pathInfo)) {
0215         const QStringList headers = NoteMapperUtils::getNoteHeaders(pathInfo);
0216         return NoteMapperUtils::getHeadersComboList(headers);
0217     }
0218 
0219     QString note = DocumentHandler::readFile(notePath + QStringLiteral("/note.md"));
0220 
0221     static const QRegularExpression block_fences =
0222         QRegularExpression(QStringLiteral("^ *(\\`{3,}|~{3,})[ \\.]*(\\S+)? *\n([\\s\\S]*?)\n? *\\1 *(?:\n+|$)"), QRegularExpression::MultilineOption);
0223     note.remove(block_fences); // Avoid things like "#include <lib>" to be catched as a heading
0224 
0225     static const QRegularExpression block_heading =
0226         QRegularExpression(QStringLiteral("^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)"), QRegularExpression::MultilineOption);
0227     QRegularExpressionMatchIterator heading_i = block_heading.globalMatch(note);
0228 
0229     QStringList headers;
0230     QRegularExpressionMatch match;
0231     while (heading_i.hasNext()) {
0232         match = heading_i.next();
0233         const QString header = match.captured(0).trimmed();
0234         headers.append(header);
0235     }
0236 
0237     updatePathInfo(cleanedPath, headers);
0238 
0239     return NoteMapperUtils::getHeadersComboList(headers);
0240 }
0241 
0242 // Treeview
0243 void NoteMapper::addInitialGlobalPaths(const QStringList &paths)
0244 {
0245     for (const auto &path : paths) {
0246         m_treeViewPaths.insert(path);
0247 
0248         if (m_savedMap.contains(path)) {
0249             m_existsMap.insert(path, m_savedMap.take(path));
0250         }
0251     }
0252     m_savedMap.clear(); // No need to preserve the rest
0253 }
0254 
0255 void NoteMapper::addGlobalPath(const QString &path)
0256 {
0257     m_treeViewPaths.insert(path);
0258 
0259     for (auto it = m_list.cbegin(); it != m_list.cend(); it++) {
0260         const auto child = static_cast<LinkedNoteItem *>(it->get());
0261 
0262         if (child->data(PathRole).toString() == path && child->data(ExistsRole) != QStringLiteral("Yes")) {
0263             child->updateExists(QStringLiteral("Yes"));
0264             // Don't need to check for header since this is a brand new file
0265             QModelIndex childIndex = createIndex(0, 0, child);
0266             Q_EMIT dataChanged(childIndex, childIndex);
0267         }
0268     }
0269 }
0270 
0271 void NoteMapper::updateGlobalPath(const QString &_oldPath, const QString &_newPath)
0272 {
0273     const auto oldPath = QString(_oldPath).remove(KleverConfig::storagePath());
0274     const auto newPath = QString(_newPath).remove(KleverConfig::storagePath());
0275     if (!m_treeViewPaths.remove(oldPath))
0276         return;
0277 
0278     if (m_existsMap.contains(oldPath))
0279         m_existsMap.insert(newPath, m_existsMap.take(oldPath));
0280 
0281     m_treeViewPaths.insert(newPath);
0282     for (auto it = m_list.cbegin(); it != m_list.cend(); it++) {
0283         const auto child = static_cast<LinkedNoteItem *>(it->get());
0284 
0285         bool needUpdate = false;
0286         if (child->data(PathRole).toString() == oldPath) {
0287             needUpdate = true;
0288             child->updatePath(newPath);
0289         }
0290         if (child->data(PathRole).toString() == newPath) {
0291             needUpdate = true;
0292             child->updateExists(QStringLiteral("Yes"));
0293         }
0294 
0295         if (needUpdate) {
0296             const QModelIndex childIndex = createIndex(0, 0, child);
0297             Q_EMIT dataChanged(childIndex, childIndex);
0298         }
0299     }
0300 }
0301 
0302 void NoteMapper::removeGlobalPath(const QString &_path)
0303 {
0304     const auto path = QString(_path).remove(KleverConfig::storagePath());
0305     if (!m_treeViewPaths.contains(path))
0306         return;
0307 
0308     m_existsMap.remove(path);
0309 
0310     m_treeViewPaths.erase(m_treeViewPaths.find(path));
0311 
0312     for (auto it = m_list.cbegin(); it != m_list.cend();) {
0313         const LinkedNoteItem *child = static_cast<LinkedNoteItem *>(it->get());
0314 
0315         if (child->data(PathRole).toString() == path) {
0316             beginRemoveRows(QModelIndex(), it - m_list.begin(), it - m_list.begin());
0317             m_list.erase(it);
0318             endRemoveRows();
0319         } else {
0320             it++;
0321         }
0322     }
0323 }
0324 
0325 // Parser
0326 void NoteMapper::addLinkedNotesInfos(const QList<QStringList> &linkedNotesInfos)
0327 {
0328     clear();
0329 
0330     for (auto infos = linkedNotesInfos.begin(); infos != linkedNotesInfos.end(); infos++) {
0331         addRow(*infos);
0332     }
0333 }
0334 
0335 void NoteMapper::updatePathInfo(const QString &path, const QStringList &headers)
0336 {
0337     const QVariantMap newPathInfo = {{QStringLiteral("headers"), QVariant(headers)}, {QStringLiteral("entirelyCheck"), QVariant(true)}};
0338 
0339     m_existsMap[path] = newPathInfo;
0340 }