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 ¬ePath) 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 }