История: словный diff в том же текстовом блоке + единая палитра

- Текст del/add парой сливается в ОДИН блок (change=mod): словный LCS по токенам, плейсхолдеры U+E000/U+E001, рендер, подстановка цветных span только в текстовых узлах; ссылки/URL/код — fallback на два блока чтобы не рвать разметку
- Единые насыщенные цвета везде: rgba(244,63,94,.45)/rgba(34,197,94,.45) — html тинты, строки кода, слова, текстовый diff, легенда (hex с альфой в QML)
- Тесты: text merge, link fallback, цвета
This commit is contained in:
2026-09-06 20:30:01 +03:00
parent e292f6586e
commit 781e1dfc12
5 changed files with 152 additions and 26 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ int main(int argc, char *argv[])
if(!diff.contains("+") || !diff.contains("-")) return fail("diff should have + -", diff);
if(!diff.contains("Версия 1") || !diff.contains("Версия 2")) return fail("diff contains versions", diff);
QString diffHtml = git.fileDiffHtml(vault, file, hash1);
if(!diffHtml.contains("background:#b7f5c6") || !diffHtml.contains("background:#ffd6dd")) return fail("diffHtml colors", diffHtml.left(500));
if(!diffHtml.contains("34,197,94") || !diffHtml.contains("244,63,94")) return fail("diffHtml colors", diffHtml.left(500));
// forward/backward: добавляем ещё файл other.md, коммитим, история note.md не должна вырасти
QString other = vault+"/other.md";
+34 -5
View File
@@ -149,19 +149,20 @@ int main(int argc, char *argv[])
const QString newMd = "# Заголовок\n\nНовая строка\n\n```cpp\nint x = 2;\n```\n";
auto d = p.parseBlocksWithDiff(oldMd, newMd, vault, fileDir);
if (d.isEmpty()) return fail("diff пуст");
// порядок как у стандартного diff: сначала все del изменённого куска, потом все add
// порядок как у стандартного diff: сначала все del изменённого куска, потом все add,
// поэтому пары del/add здесь не соседствуют и merge не срабатывает — отдельные блоки
auto changeOf = [&](int i) { return d[i].toMap().value("change").toString(); };
if (d.size() != 5) return fail("diff размер", QString::number(d.size()));
if (changeOf(0) != "same") return fail("diff[0] должен быть same", changeOf(0));
if (d[0].toMap().value("type").toString() != "html") return fail("diff[0] тип");
if (changeOf(1) != "del") return fail("diff[1] должен быть del", changeOf(1));
if (!d[1].toMap().value("html").toString().contains("ffd6dd")) return fail("diff[1] без розовой подсветки");
if (!d[1].toMap().value("html").toString().contains("244,63,94")) return fail("diff[1] без розовой подсветки");
if (changeOf(2) != "del" || d[2].toMap().value("type").toString() != "code")
return fail("diff[2] код del", changeOf(2));
if (!d[2].toMap().value("highlightedText").toString().contains("244,63,94"))
return fail("diff[2] код без розовой подсветки");
if (changeOf(3) != "add") return fail("diff[3] должен быть add", changeOf(3));
if (!d[3].toMap().value("html").toString().contains("b7f5c6")) return fail("diff[3] без зелёной подсветки");
if (!d[3].toMap().value("html").toString().contains("34,197,94")) return fail("diff[3] без зелёной подсветки");
if (changeOf(4) != "add" || d[4].toMap().value("type").toString() != "code")
return fail("diff[4] код add", changeOf(4));
// кодовые блоки сохранили id для копирования
@@ -177,12 +178,40 @@ int main(int argc, char *argv[])
for (const auto &v : d) {
if (v.toMap().value("change").toString() != "same")
return fail("identical должен быть same");
if (v.toMap().value("html").toString().contains("ffd6dd")
|| v.toMap().value("html").toString().contains("b7f5c6"))
if (v.toMap().value("html").toString().contains("244,63,94")
|| v.toMap().value("html").toString().contains("34,197,94"))
return fail("identical с подсветкой");
}
}
// parseBlocksWithDiff: правка в обычном тексте — ОДИН блок со словным diff внутри
{
const QString oldMd = "# T\n\nOld words here\n";
const QString newMd = "# T\n\nNew words here\n";
auto d = p.parseBlocksWithDiff(oldMd, newMd, vault, fileDir);
if (d.size() != 2) return fail("text merge размер", QString::number(d.size()));
if (d[0].toMap().value("change").toString() != "same") return fail("text merge[0] same");
if (d[1].toMap().value("type").toString() != "html"
|| d[1].toMap().value("change").toString() != "mod")
return fail("text merge[1] должен быть html/mod", d[1].toMap().value("change").toString());
const QString html = d[1].toMap().value("html").toString();
if (!html.contains("244,63,94")) return fail("text merge без розового");
if (!html.contains("34,197,94")) return fail("text merge без зелёного");
if (!html.contains("Old") || !html.contains("New")) return fail("text merge потерял слова", html.left(300));
if (!html.contains("words here")) return fail("text merge потерял общее", html.left(300));
}
// parseBlocksWithDiff: ссылка поменялась — fallback на два отдельных блока (разметку не рвём)
{
const QString oldMd = "# T\n\n[a](http://old.com/x)\n";
const QString newMd = "# T\n\n[a](http://new.com/x)\n";
auto d = p.parseBlocksWithDiff(oldMd, newMd, vault, fileDir);
if (d.size() != 3) return fail("link fallback размер", QString::number(d.size()));
if (d[1].toMap().value("change").toString() != "del"
|| d[2].toMap().value("change").toString() != "add")
return fail("link fallback del/add");
}
// parseBlocksWithDiff: правка внутри кода — ОДИН блок с построчным diff внутри
{
const QString oldMd = "# T\n\n```cpp\nint a = 1;\nint b = 2;\n```\n";