Упрощение git: только просмотр с diff, без словного уровня, render без побочек

- История: убран таб текстового diff — только Просмотр версии с подсветкой; пустые состояния через emptyMessage
- GitManager: удалены fileDiffText/fileDiffHtml (+escHtml), остался fileContentAt/workdirContent/history/restore
- MdParser: удалён словный merge (плейсхолдеры/tokenize/risiko), текст — отдельными del/add как diff; renderInner без сброса codeBlocks, render() — clear+renderInner; убраны save/restore хаки
- Тесты обновлены
This commit is contained in:
2026-09-06 20:49:14 +03:00
parent 781e1dfc12
commit f6074df0fc
7 changed files with 67 additions and 360 deletions
+21 -135
View File
@@ -217,37 +217,6 @@ QString tintDiffHtml(const QString &inner, bool isDel)
return QStringLiteral("<div style=\"background-color:rgba(34,197,94,0.45);border-left:3px solid #1a7f37;border-radius:6px;padding:6px 8px;margin:4px 0\">%1</div>").arg(inner);
}
// Токенизация на слова/пробелы для словного diff (границы слов не рвём)
QStringList tokenizeWords(const QString &s)
{
QStringList toks;
QString cur;
bool curSpace = false;
bool first = true;
for (const QChar &c : s) {
const bool sp = c.isSpace();
if (first) { cur += c; curSpace = sp; first = false; continue; }
if (sp == curSpace)
cur += c;
else {
toks.append(cur);
cur = QString(c);
curSpace = sp;
}
}
if (!cur.isEmpty())
toks.append(cur);
return toks;
}
// Блоки, где встроенный словный diff опасен (ссылки/URL/код — плейсхолдеры
// порвут разметку): для них оставляем два отдельных tinted-блока
bool riskyInlineMerge(const QString &block)
{
return block.contains(QLatin1Char('`')) || block.contains(QLatin1String("]("))
|| block.contains(QLatin1String("://"));
}
QString fileHrefFor(const QString &path)
{
const QByteArray enc = QUrl::toPercentEncoding(QDir::cleanPath(path));
@@ -876,6 +845,17 @@ private:
QString MdParser::render(const QString &markdown, const QString &vaultRoot,
const QString &fileDir)
{
// Публичный вызов начинает нумерацию кодовых блоков заново (code_0, code_1, ...)
if (m) {
m->codeBlocks.clear();
m->codeCounter = 0;
}
return renderInner(markdown, vaultRoot, fileDir);
}
QString MdParser::renderInner(const QString &markdown, const QString &vaultRoot,
const QString &fileDir)
{
cmark_gfm_core_extensions_ensure_registered();
cmark_parser *parser = cmark_parser_new(CMARK_OPT_DEFAULT);
@@ -888,10 +868,6 @@ QString MdParser::render(const QString &markdown, const QString &vaultRoot,
cmark_parser_feed(parser, utf8.constData(), static_cast<size_t>(utf8.size()));
cmark_node *doc = cmark_parser_finish(parser);
if (m) {
m->codeBlocks.clear();
m->codeCounter = 0;
}
Renderer r(m, vaultRoot, fileDir);
QString html = r.renderTree(doc);
@@ -923,12 +899,7 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
if (start > lastPos) {
QString htmlPart = markdown.mid(lastPos, start - lastPos);
if (!htmlPart.trimmed().isEmpty()) {
// Сохраняем codeBlocks, т.к. render() их очищает
QHash<QString, QString> savedBlocks;
int savedCounter = 0;
if (m) { savedBlocks = m->codeBlocks; savedCounter = m->codeCounter; }
QString html = render(htmlPart, vaultRoot, fileDir);
if (m) { m->codeBlocks = savedBlocks; m->codeCounter = savedCounter; }
QString html = renderInner(htmlPart, vaultRoot, fileDir);
if (!html.trimmed().isEmpty()) {
QVariantMap block;
block["type"] = "html";
@@ -989,11 +960,7 @@ QVariantList MdParser::parseBlocks(const QString &markdown, const QString &vault
}
// Если вообще не нашли блоки, но markdown не пустой — вернём как html
if (blocks.isEmpty() && !markdown.trimmed().isEmpty()) {
QHash<QString, QString> savedBlocks;
int savedCounter = 0;
if (m) { savedBlocks = m->codeBlocks; savedCounter = m->codeCounter; }
QString html = render(markdown, vaultRoot, fileDir);
if (m) { m->codeBlocks = savedBlocks; m->codeCounter = savedCounter; }
QString html = renderInner(markdown, vaultRoot, fileDir);
QVariantMap block;
block["type"] = "html";
block["html"] = html;
@@ -1025,16 +992,6 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
if (oldMd.isEmpty() && newMd.isEmpty())
return blocks;
// render() внутри очищает codeBlocks — сохраняем/восстанавливаем вокруг вызовов
auto renderKeep = [&](const QString &part) -> QString {
QHash<QString, QString> savedBlocks;
int savedCounter = 0;
if (m) { savedBlocks = m->codeBlocks; savedCounter = m->codeCounter; }
QString html = render(part, vaultRoot, fileDir);
if (m) { m->codeBlocks = savedBlocks; m->codeCounter = savedCounter; }
return html;
};
auto appendHtmlBlock = [&](const QString &html, const char *change) {
QVariantMap block;
block["type"] = "html";
@@ -1131,73 +1088,6 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
return true;
};
// Обычный текст (del+add парой): один блок со словным diff внутри через
// плейсхолдеры — разметка не рвётся, подстановка только в текстовых узлах
auto appendMergedText = [&](const QString &oldT, const QString &newT) -> bool {
if (riskyInlineMerge(oldT) || riskyInlineMerge(newT))
return false;
const QStringList a = tokenizeWords(oldT);
const QStringList b = tokenizeWords(newT);
if (a.isEmpty() || b.isEmpty())
return false;
if ((long long)a.size() * b.size() > 250000)
return false;
const QVector<DiffOp> wops = diffMdBlocks(a, b);
struct Ph { QString token; QString word; bool isDel; };
QVector<Ph> phs;
QString merged;
int phId = 0;
bool hasChange = false;
for (const DiffOp &w : wops) {
if (w.kind == '=') {
merged += w.text;
} else {
hasChange = true;
const QString ph = QString(QChar(0xE000)) + QString::number(phId++)
+ QString(QChar(0xE001));
merged += ph;
phs.append({ph, w.text, w.kind == '-'});
}
}
if (!hasChange)
return false;
QString html = renderKeep(merged);
if (html.trimmed().isEmpty())
return false;
// подстановка плейсхолдеров: в текстовых узлах — цветной span,
// внутри тегов (ссылки/атрибуты) — просто слово, чтобы не ломать разметку
for (const Ph &ph : phs) {
const QString styled = QStringLiteral("<span style=\"background-color:%1;border-radius:3px;padding:0 2px;\">%2</span>")
.arg(ph.isDel ? QStringLiteral("rgba(244,63,94,0.45)")
: QStringLiteral("rgba(34,197,94,0.45)"),
esc(ph.word));
const QString plain = esc(ph.word);
QString out;
out.reserve(html.size() + styled.size());
int pos = 0;
bool found = false;
while (true) {
const int f = html.indexOf(ph.token, pos);
if (f < 0) {
out += html.mid(pos);
break;
}
found = true;
const int lt = html.lastIndexOf(QLatin1Char('<'), f);
const int gt = (lt >= 0) ? html.indexOf(QLatin1Char('>'), lt) : -1;
const bool inTag = (lt >= 0 && (gt < 0 || gt > f));
out += html.mid(pos, f - pos);
out += inTag ? plain : styled;
pos = f + ph.token.length();
}
if (!found)
return false;
html = out;
}
appendHtmlBlock(html, "mod");
return true;
};
const QVector<DiffOp> ops = diffMdBlocks(splitMdBlocks(oldMd), splitMdBlocks(newMd));
for (int k = 0; k < ops.size(); ++k) {
const DiffOp &op = ops[k];
@@ -1206,16 +1096,12 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
const char *change = op.kind == '=' ? "same" : (isDel ? "del" : "add");
QString lang, code;
const bool isCode = isFenceBlock(op.text) && parseFenceBlock(op.text, &lang, &code);
if (isDel && k + 1 < ops.size() && ops[k + 1].kind == '+') {
if (isCode) {
QString nlang, ncode;
if (isFenceBlock(ops[k + 1].text) && parseFenceBlock(ops[k + 1].text, &nlang, &ncode)
&& appendMergedCode(lang, code, nlang, ncode)) {
++k;
continue;
}
} else if (!isFenceBlock(ops[k + 1].text)
&& appendMergedText(op.text, ops[k + 1].text)) {
// Изменённый код — один блок с построчным diff внутри; обычный текст —
// отдельными tinted-блоками (del, затем add), как стандартный diff
if (isCode && isDel && k + 1 < ops.size() && ops[k + 1].kind == '+') {
QString nlang, ncode;
if (isFenceBlock(ops[k + 1].text) && parseFenceBlock(ops[k + 1].text, &nlang, &ncode)
&& appendMergedCode(lang, code, nlang, ncode)) {
++k;
continue;
}
@@ -1223,7 +1109,7 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
if (isCode) {
appendCodeBlock(lang, code, change, changed);
} else {
QString html = renderKeep(op.text);
QString html = renderInner(op.text, vaultRoot, fileDir);
if (html.trimmed().isEmpty())
continue;
if (changed)
@@ -1235,7 +1121,7 @@ QVariantList MdParser::parseBlocksWithDiff(const QString &oldMd, const QString &
// Пустой результат при непустом вводе — отдать как один html-блок
if (blocks.isEmpty() && (!oldMd.trimmed().isEmpty() || !newMd.trimmed().isEmpty())) {
const QString src = !oldMd.trimmed().isEmpty() ? oldMd : newMd;
const QString html = renderKeep(src);
const QString html = renderInner(src, vaultRoot, fileDir);
if (!html.trimmed().isEmpty())
appendHtmlBlock(html, "same");
}
+6
View File
@@ -42,5 +42,11 @@ public:
struct Impl;
private:
// Внутренний рендер без сброса codeBlocks/codeCounter (для parseBlocks*,
// которые сами управляют состоянием и вызывают его многократно)
QString renderInner(const QString &markdown,
const QString &vaultRoot,
const QString &fileDir);
Impl *m = nullptr;
};