Files
Flora/src/core/VaultIndex.cpp
T

240 lines
7.3 KiB
C++
Raw Normal View History

#include "core/VaultIndex.h"
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
namespace {
QStringList kIgnoredNames = {".git", ".flora", ".stfolder"};
// Извлекает теги #tag из текста (пропуская блоки кода).
QStringList extractTags(const QString &text)
{
QStringList tags;
static const QRegularExpression tagRe(
QStringLiteral("(?:^|[^\\w#])#([\\p{L}\\w][\\p{L}\\w-]*)"));
bool inCode = false;
const QStringList lines = text.split(QLatin1Char('\n'));
for (const QString &line : lines) {
const QString t = line.trimmed();
if (t.startsWith(QLatin1String("```"))) {
inCode = !inCode;
continue;
}
if (inCode)
continue;
// Заголовок "# Заголовок" — не тег (после # идёт пробел)
if (t.startsWith(QLatin1String("# ")) || t.startsWith(QLatin1String("## ")))
continue;
auto it = tagRe.globalMatch(line);
while (it.hasNext()) {
const QString tag = it.next().captured(1);
if (!tags.contains(tag))
tags.append(tag);
}
}
return tags;
}
QString cleanDir(const QString &p)
{
QString s = p;
while (s.endsWith(QLatin1Char('/')))
s.chop(1);
return s;
}
} // namespace
VaultIndex::VaultIndex(QObject *parent)
: QObject(parent)
{
}
QString VaultIndex::displayName(const QString &nodeKey) const
{
if (nodeKey.startsWith(QLatin1String("tag:")))
return nodeKey.mid(4);
const int idx = nodeKey.lastIndexOf(QLatin1Char('/'));
return idx >= 0 ? nodeKey.mid(idx + 1) : nodeKey;
}
QString VaultIndex::nodeKeyFor(const QString &path, bool isFile) const
{
return isFile ? QDir::cleanPath(path) : QStringLiteral("tag:%1").arg(path);
}
int VaultIndex::ensureNode(const QString &key, const QString &name, const QString &type)
{
auto it = m_nodeById.constFind(key);
if (it != m_nodeById.constEnd())
return it.value();
QVariantMap node;
node["key"] = key;
node["name"] = name;
node["type"] = type;
m_nodes.append(node);
const int id = m_nodes.size() - 1;
m_nodeById.insert(key, id);
return id;
}
void VaultIndex::addEdge(const QString &from, const QString &to)
{
if (from == to)
return;
QVariantMap edge;
edge["from"] = from;
edge["to"] = to;
m_edges.append(edge);
}
QString VaultIndex::resolveLink(const QString &fileDir, const QString &target) const
{
QString t = target.trimmed();
if (t.isEmpty())
return QString();
if (t.contains(QLatin1String("://")))
return QString(); // внешняя
QStringList cands;
cands << cleanDir(fileDir) + '/' + t
<< cleanDir(fileDir) + '/' + t + ".md";
for (const QString &c : cands)
if (QFileInfo::exists(c))
return QDir::cleanPath(c);
// поиск по имени файла по всему хранилищу (без расширения)
auto it = m_fileByBase.constFind(t);
if (it != m_fileByBase.constEnd())
return it.value();
it = m_fileByBase.constFind(t + ".md");
if (it != m_fileByBase.constEnd())
return it.value();
return QString();
}
void VaultIndex::rebuild(const QString &vault)
{
m_busy = true;
emit busyChanged();
m_nodes.clear();
m_edges.clear();
m_nodeById.clear();
m_tagsOfFile.clear();
m_linksOfFile.clear();
m_filesWithTag.clear();
m_fileByBase.clear();
m_fileCount = 0;
// Проход 1: собрать все md-файлы и карту имён
QStringList files;
collectFiles(vault, 0, files);
m_fileCount = files.size();
for (const QString &f : files) {
const QString base = QFileInfo(f).completeBaseName();
if (!base.isEmpty() && !m_fileByBase.contains(base))
m_fileByBase.insert(base, f);
}
// Проход 2: узлы/теги/ссылки
for (const QString &f : files)
parseFile(f);
m_busy = false;
emit busyChanged();
emit indexChanged();
}
void VaultIndex::collectFiles(const QString &dir, int depth, QStringList &out) const
{
if (depth > 24)
return;
QDir d(dir);
const QFileInfoList entries = d.entryInfoList(
QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::NoSymLinks);
for (const QFileInfo &fi : entries) {
if (kIgnoredNames.contains(fi.fileName()))
continue;
if (fi.isDir())
collectFiles(fi.absoluteFilePath(), depth + 1, out);
else if (fi.suffix().compare(QLatin1String("md"), Qt::CaseInsensitive) == 0)
out.append(fi.absoluteFilePath());
}
}
void VaultIndex::parseFile(const QString &path)
{
QFile f(path);
if (!f.open(QIODevice::ReadOnly))
return;
const QString text = QString::fromUtf8(f.readAll());
f.close();
// m_fileCount уже установлен в rebuild (files.size())
const QString fileDir = QFileInfo(path).absolutePath();
const QString key = nodeKeyFor(path, true);
ensureNode(key, QFileInfo(path).fileName(), QStringLiteral("file"));
{
const QString base = QFileInfo(path).completeBaseName();
if (!base.isEmpty() && !m_fileByBase.contains(base))
m_fileByBase.insert(base, path);
}
// Теги
const QStringList tags = extractTags(text);
m_tagsOfFile.insert(path, tags);
for (const QString &tag : tags) {
const QString tagKey = nodeKeyFor(tag, false);
ensureNode(tagKey, QStringLiteral("#%1").arg(tag), QStringLiteral("tag"));
addEdge(key, tagKey);
m_filesWithTag[tag].append(path);
}
// Ссылки [[X]] и [text](rel)
QVariantList links;
static const QRegularExpression wikiRe(QStringLiteral("\\[\\[([^\\]]+)\\]\\]"));
auto wi = wikiRe.globalMatch(text);
while (wi.hasNext()) {
const QString target = wi.next().captured(1);
const QString resolved = resolveLink(fileDir, target);
if (!resolved.isEmpty()) {
links.append(QVariantMap{{"text", target}, {"path", resolved}});
addEdge(key, nodeKeyFor(resolved, true));
ensureNode(nodeKeyFor(resolved, true),
QFileInfo(resolved).fileName(), QStringLiteral("file"));
}
}
static const QRegularExpression mdLinkRe(
QStringLiteral("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"));
auto mi = mdLinkRe.globalMatch(text);
while (mi.hasNext()) {
auto m = mi.next();
const QString target = m.captured(2);
if (target.startsWith(QLatin1String("#")))
continue;
const QString resolved = resolveLink(fileDir, target);
if (!resolved.isEmpty()) {
links.append(QVariantMap{{"text", m.captured(1)}, {"path", resolved}});
addEdge(key, nodeKeyFor(resolved, true));
ensureNode(nodeKeyFor(resolved, true),
QFileInfo(resolved).fileName(), QStringLiteral("file"));
}
}
m_linksOfFile.insert(path, links);
}
QStringList VaultIndex::tagsOfFile(const QString &path) const
{
return m_tagsOfFile.value(path);
}
QVariantList VaultIndex::linksOfFile(const QString &path) const
{
return m_linksOfFile.value(path);
}
QStringList VaultIndex::filesWithTag(const QString &tag) const
{
return m_filesWithTag.value(tag);
}