diff --git a/qml/dialogs/HistoryDialog.qml b/qml/dialogs/HistoryDialog.qml index 0fdf06d..0df6c93 100644 --- a/qml/dialogs/HistoryDialog.qml +++ b/qml/dialogs/HistoryDialog.qml @@ -28,9 +28,9 @@ Dialog { property var historyModel: [] property int selectedIndex: -1 property string selectedHash: "" - property string diffHtml: "" property string versionContent: "" property string currentContent: "" + property string emptyMessage: qsTr("Нет версии для просмотра") property bool busy: false function fileDirOf(p) { @@ -62,7 +62,7 @@ Dialog { historyModel = [] versionContent = "" currentContent = "" - diffHtml = "

" + qsTr("Файл не выбран") + "

" + emptyMessage = qsTr("Файл не выбран") return } busy = true @@ -79,16 +79,11 @@ Dialog { selectedHash = "" versionContent = "" currentContent = "" - const hasFile = filePath.length>0 - if (hasFile) { - const st = gitManager.fileGitStatus(vaultPath, filePath) - if (st === "неотслеживается" || st === "новый") { - diffHtml = "

" + qsTr("История пуста — файл ещё не сохранён в git. Сохраните файл, и здесь появится первая версия.") + "

" - } else { - diffHtml = "

" + qsTr("Нет версий для этого файла") + "

" - } + const st = gitManager.fileGitStatus(vaultPath, filePath) + if (st === "неотслеживается" || st === "новый") { + emptyMessage = qsTr("История пуста — файл ещё не сохранён в git. Сохраните файл, и здесь появится первая версия.") } else { - diffHtml = "" + emptyMessage = qsTr("Нет версий для этого файла") } } busy = false @@ -96,16 +91,12 @@ Dialog { function updateDiff() { if (selectedHash.length===0) { - diffHtml = "" versionContent = "" currentContent = "" return } // свежее содержимое с диска — diff всегда актуален, даже если файл правили при открытом диалоге currentContent = gitManager.workdirContent(filePath) - const html = gitManager.fileDiffHtml(vaultPath, filePath, selectedHash) - if (html.length===0) diffHtml = "

" + qsTr("Нет изменений") + "

" - else diffHtml = html // содержимое версии — сырой markdown, отрисуется через MarkdownView как в «Просмотре», // с подсветкой отличий от текущего файла прямо там versionContent = gitManager.fileContentAt(vaultPath, filePath, selectedHash) @@ -292,87 +283,48 @@ Dialog { } } - // Правая панель — diff и превью + // Правая панель — версия как кнопка «Просмотр», с подсветкой отличий прямо там ColumnLayout { SplitView.fillWidth: true SplitView.minimumWidth: 300 spacing: 0 - TabBar { - id: tabBar - Layout.fillWidth: true - TabButton { text: qsTr("Изменения") } - TabButton { text: qsTr("Просмотр версии") } - } - - StackLayout { + Rectangle { Layout.fillWidth: true Layout.fillHeight: true - currentIndex: tabBar.currentIndex - - // Diff вкладка - Rectangle { - color: Theme.editorBg - border.color: Theme.divider - ScrollView { - anchors.fill: parent - anchors.margins: 6 - clip: true - ScrollBar.vertical: ScrollBar {} - TextArea { - id: diffArea - readOnly: true - textFormat: Text.RichText - wrapMode: Text.Wrap - color: Theme.text - selectionColor: Theme.selectionBg - text: root.diffHtml - background: Rectangle { color: Theme.editorBg } - font.family: "Menlo, monospace" - font.pixelSize: 12 - } - } - } - - // Просмотр версии — как кнопка «Просмотр» в редакторе - Rectangle { - color: Theme.editorBg - border.color: Theme.divider - ScrollView { - id: versionScroll - anchors.fill: parent - anchors.margins: 6 - clip: true - ScrollBar.vertical: ScrollBar {} - Column { - width: versionScroll.availableWidth - MarkdownView { - id: versionPreview - width: parent.width - markdown: root.versionContent - compareMarkdown: root.currentContent - vaultRoot: root.vaultPath - fileDir: root.fileDirOf(root.filePath) - emptyText: qsTr("Нет версии для просмотра") - onLinkActivated: (href) => root.handleHistoryLink(href) - } + color: Theme.editorBg + border.color: Theme.divider + ScrollView { + id: versionScroll + anchors.fill: parent + anchors.margins: 6 + clip: true + ScrollBar.vertical: ScrollBar {} + Column { + width: versionScroll.availableWidth + MarkdownView { + id: versionPreview + width: parent.width + markdown: root.versionContent + compareMarkdown: root.currentContent + vaultRoot: root.vaultPath + fileDir: root.fileDirOf(root.filePath) + emptyText: root.emptyMessage + onLinkActivated: (href) => root.handleHistoryLink(href) } } } } - // Легенда — цвета отличий от текущего файла (обе вкладки) + // Легенда — цвета отличий от текущего файла Row { Layout.fillWidth: true Layout.margins: 6 spacing: 12 Row { spacing: 4; Rectangle { width:12; height:12; color:"#7322C55E"; border.color:"#1a7f37"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("добавлено"); color: Theme.textMuted; font.pixelSize:10 } } Row { spacing: 4; Rectangle { width:12; height:12; color:"#73F43F5E"; border.color:"#e5484d"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("удалено"); color: Theme.textMuted; font.pixelSize:10 } } - Row { visible: tabBar.currentIndex === 0; spacing: 4; Rectangle { width:12; height:12; color:"#ddf4ff"; border.color:"#0969da"; radius:2; anchors.verticalCenter: parent.verticalCenter } Label { text: qsTr("заголовок"); color: Theme.textMuted; font.pixelSize:10 } } Label { - text: tabBar.currentIndex === 0 - ? qsTr("Сравнение выбранной версии с текущим файлом") - : qsTr("Версия с подсветкой отличий от текущего файла") + text: qsTr("Версия с подсветкой отличий от текущего файла") color: Theme.textFaint font.pixelSize: 10 Layout.fillWidth: true diff --git a/src/git/GitManager.cpp b/src/git/GitManager.cpp index 351da96..07e0956 100644 --- a/src/git/GitManager.cpp +++ b/src/git/GitManager.cpp @@ -41,23 +41,6 @@ QString timeAgoString(const QDateTime &dt) return dt.toString(QStringLiteral("dd.MM.yyyy hh:mm")); } -QString escHtml(const QString &s) -{ - QString out; - out.reserve(s.size()); - for (const QChar &c : s) { - switch (c.unicode()) { - case '&': out += "&"; break; - case '<': out += "<"; break; - case '>': out += ">"; break; - case '"': out += """; break; - case '\'': out += "'"; break; - default: out += c; break; - } - } - return out; -} - } // namespace bool GitManager::s_gitInitialized = false; @@ -505,90 +488,6 @@ QString GitManager::fileContentAt(const QString &vaultPath, const QString &fileP return result; } -QString GitManager::fileDiffText(const QString &vaultPath, const QString &filePath, const QString &commitHash) const -{ - const QString oldContent = fileContentAt(vaultPath, filePath, commitHash); - QFile f(filePath); - QString newContent; - if (f.open(QIODevice::ReadOnly)) - newContent = QString::fromUtf8(f.readAll()); - else - newContent = QString(); - - if (oldContent == newContent) - return QStringLiteral("Нет изменений"); - - // Используем git_patch_from_buffers для диффа - git_patch *patch = nullptr; - const QByteArray oldBytes = oldContent.toUtf8(); - const QByteArray newBytes = newContent.toUtf8(); - const QString rel = relativePath(vaultPath, filePath); - const QByteArray relBytes = rel.toUtf8(); - git_patch_from_buffers(&patch, - oldBytes.constData(), (size_t)oldBytes.size(), relBytes.constData(), - newBytes.constData(), (size_t)newBytes.size(), relBytes.constData(), - nullptr); - if (!patch) - return QStringLiteral("Не удалось построить diff"); - - QString out; - // Заголовок - out += QStringLiteral("--- a/%1\n").arg(rel); - out += QStringLiteral("+++ b/%1\n").arg(rel); - - size_t hunks = git_patch_num_hunks(patch); - for (size_t h = 0; h < hunks; ++h) { - const git_diff_hunk *hk = nullptr; - size_t hunkLines = 0; - if (git_patch_get_hunk(&hk, &hunkLines, patch, h) < 0) continue; - out += QString::fromUtf8(hk->header, (int)hk->header_len); - if (!out.endsWith('\n')) out += '\n'; - for (size_t l = 0; l < hunkLines; ++l) { - const git_diff_line *line = nullptr; - if (git_patch_get_line_in_hunk(&line, patch, h, l) < 0) continue; - QChar origin = QChar(line->origin); - QString text = QString::fromUtf8(line->content, (int)line->content_len); - // line->content уже содержит \n в конце - out += origin + text; - if (!text.endsWith('\n') && line->content_len > 0) out += '\n'; - } - } - git_patch_free(patch); - if (out.trimmed().isEmpty()) - return QStringLiteral("Нет изменений"); - return out; -} - -QString GitManager::fileDiffHtml(const QString &vaultPath, const QString &filePath, const QString &commitHash) const -{ - const QString txt = fileDiffText(vaultPath, filePath, commitHash); - if (txt == QStringLiteral("Нет изменений") || txt.startsWith(QStringLiteral("Не удалось"))) - return QStringLiteral("

%1

").arg(escHtml(txt)); - - QString html; - html.reserve(txt.size()*2); - html += "
";
-    const QStringList lines = txt.split('\n');
-    for (const QString &rawLine : lines) {
-        if (rawLine.isEmpty()) { html += "\n"; continue; }
-        QChar first = rawLine.at(0);
-        QString esc = escHtml(rawLine);
-        if (first == '+') {
-            html += QStringLiteral("
%1
").arg(esc); - } else if (first == '-') { - html += QStringLiteral("
%1
").arg(esc); - } else if (first == '@') { - html += QStringLiteral("
%1
").arg(esc); - } else if (first == ' ') { - html += QStringLiteral("
%1
").arg(esc); - } else { - html += QStringLiteral("
%1
").arg(esc); - } - } - html += "
"; - return html; -} - QString GitManager::restoreFile(const QString &vaultPath, const QString &filePath, const QString &commitHash) { const QString vault = QDir::cleanPath(vaultPath); diff --git a/src/git/GitManager.h b/src/git/GitManager.h index 3de0810..a1dee87 100644 --- a/src/git/GitManager.h +++ b/src/git/GitManager.h @@ -41,11 +41,6 @@ public: // Текущее содержимое файла на диске (для сравнения с версией). Пусто при ошибке/лимите 2 МБ. Q_INVOKABLE QString workdirContent(const QString &filePath) const; - // Diff между версией в коммите и текущим файлом (рабочее дерево). Возвращает HTML с цветами. - Q_INVOKABLE QString fileDiffHtml(const QString &vaultPath, const QString &filePath, const QString &commitHash) const; - // Текстовый diff (unified) - Q_INVOKABLE QString fileDiffText(const QString &vaultPath, const QString &filePath, const QString &commitHash) const; - // Восстановить файл из коммита (перезаписать и закоммитить). Возвращает "" при успехе или текст ошибки. Q_INVOKABLE QString restoreFile(const QString &vaultPath, const QString &filePath, const QString &commitHash); diff --git a/src/markdown/MdParser.cpp b/src/markdown/MdParser.cpp index bbd4608..0afa8e3 100644 --- a/src/markdown/MdParser.cpp +++ b/src/markdown/MdParser.cpp @@ -217,37 +217,6 @@ QString tintDiffHtml(const QString &inner, bool isDel) return QStringLiteral("
%1
").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(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 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 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 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 wops = diffMdBlocks(a, b); - struct Ph { QString token; QString word; bool isDel; }; - QVector 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("%2") - .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 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"); } diff --git a/src/markdown/MdParser.h b/src/markdown/MdParser.h index 3ac6c03..edac4e3 100644 --- a/src/markdown/MdParser.h +++ b/src/markdown/MdParser.h @@ -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; }; \ No newline at end of file diff --git a/tests/git_history_test.cpp b/tests/git_history_test.cpp index 0694985..4431461 100644 --- a/tests/git_history_test.cpp +++ b/tests/git_history_test.cpp @@ -66,14 +66,8 @@ int main(int argc, char *argv[]) // content at QString c1 = git.fileContentAt(vault, file, hash1); if(!c1.contains("Версия 1")) return fail("content v1", c1); - // diff с текущим (одинаково) должен быть "Нет изменений" или пусто? - QString diffSame = git.fileDiffText(vault, file, hash1); - if(diffSame != "Нет изменений" && !diffSame.isEmpty()) { - // если файл не менялся, diff должен быть "Нет изменений" - // Но мы ещё не меняли, так что должно быть нет изменений - if(!diffSame.contains("Нет изменений")) - return fail("diff same should be нет изменений", diffSame); - } + // workdirContent читает текущее с диска + if(git.workdirContent(file) != "Версия 1\nСтрока A\n") return fail("workdirContent", git.workdirContent(file)); // Изменяем файл → v2 QThread::msleep(1100); // чтобы время коммита отличалось @@ -95,12 +89,6 @@ int main(int argc, char *argv[]) // content at hash1 всё ещё v1 QString c1again = git.fileContentAt(vault, file, hash1); if(!c1again.contains("Версия 1") || c1again.contains("Строка B")) return fail("c1again", c1again); - // diff hash1 vs current (v2): должен показать + строка B и -Версия1 +Версия2 - QString diff = git.fileDiffText(vault, file, hash1); - 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("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"; @@ -140,18 +128,10 @@ int main(int argc, char *argv[]) if(hist5.size()!=5) return fail("hist 5", QString::number(hist5.size())); if(hist5.first().toMap().value("message").toString()!="v3") return fail("top v3"); - // Проверка что diff между v1 и текущим (v3) показывает изменения - QString diffV1Now = git.fileDiffText(vault, file, hash1); - if(!diffV1Now.contains("Версия 1") || !diffV1Now.contains("Версия 3")) return fail("diff v1->v3", diffV1Now); - // Проверка несуществующего коммита QString badContent = git.fileContentAt(vault, file, "0000000000000000000000000000000000000000"); if(!badContent.isEmpty()) return fail("bad hash should return empty"); - QString badDiff = git.fileDiffText(vault, file, "0000000000000000000000000000000000000000"); - // должен либо вернуть ошибку diff, либо содержать old empty - // не проверяем строго - QString badRestore = git.restoreFile(vault, file, "0000000000000000000000000000000000000000"); if(badRestore.isEmpty()) return fail("bad restore should fail"); diff --git a/tests/mdparser_test.cpp b/tests/mdparser_test.cpp index f222ff1..d1b7b10 100644 --- a/tests/mdparser_test.cpp +++ b/tests/mdparser_test.cpp @@ -184,32 +184,21 @@ int main(int argc, char *argv[]) } } - // parseBlocksWithDiff: правка в обычном тексте — ОДИН блок со словным diff внутри + // parseBlocksWithDiff: правка в обычном тексте — два отдельных tinted-блока + // (del, затем add), как стандартный 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.size() != 3) return fail("text diff размер", QString::number(d.size())); + if (d[0].toMap().value("change").toString() != "same") return fail("text diff[0] same"); if (d[1].toMap().value("change").toString() != "del" || d[2].toMap().value("change").toString() != "add") - return fail("link fallback del/add"); + return fail("text diff del/add"); + if (!d[1].toMap().value("html").toString().contains("244,63,94")) + return fail("text del без розового"); + if (!d[2].toMap().value("html").toString().contains("34,197,94")) + return fail("text add без зелёного"); } // parseBlocksWithDiff: правка внутри кода — ОДИН блок с построчным diff внутри