Files
Flora/tests/mdparser_test.cpp
T
grendervill 417d0273b7 Инкрементальный индекс, пресеты автосейва, диалог Настройки
- VaultIndex: updateFile/removeFile/syncIncremental (mtime+size), полный рескан только на старте/смене/внешних изменениях; relink висячих [[ссылок]] при создании файла; FileStore.externalChanged для внешних правок
- QML: сейвы и правки идут точечно, дерево — через syncIncremental
- Настройки: пресеты safe/balanced/eco/manual (диск 2/5/10с/выкл, коммит после паузы 5/15/60с/вручную), диалог с +/-, шкалами и подсказкой; коммит только в простое, в manual — по Ctrl+S
- Удалены highlightedCode, словный merge, fileDiffText/Html, 2-arg fileHistory; renderInner без побочек
- Тесты: инкремент индекса, пресеты, autosaveEnabled, ~~~-блоки
2026-09-06 23:02:05 +03:00

270 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Тест MdParser (Фаза 6): рендер Markdown → HTML
// (заголовки, жирный/курсив/зачёркнутый, таблицы, ссылки, картинки,
// задачи с переключаемыми ссылками, подсветка кода, вики-ссылки).
#include <QCoreApplication>
#include <QTemporaryDir>
#include <QDir>
#include <QFile>
#include <QEventLoop>
#include <QTimer>
#include <cstdio>
#include "markdown/MdParser.h"
static int fail(const char *msg, const QString &detail = QString())
{
std::fprintf(stderr, "FAIL: %s | %s\n", msg, qPrintable(detail));
return 1;
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QTemporaryDir dir;
if (!dir.isValid())
return fail("temp dir");
const QString vault = dir.path();
QDir(vault).mkpath("sub");
{
QFile f(vault + "/sub/b.md");
f.open(QIODevice::WriteOnly);
f.write("b");
f.close();
QFile g(vault + "/sub/pic.png");
g.open(QIODevice::WriteOnly);
g.write("\x89PNG fake");
g.close();
}
MdParser p;
const QString fileDir = vault + "/sub";
auto probe = [](const QString &html, const char *sub) {
return html.contains(QString::fromUtf8(sub));
};
// Заголовок
QString h = p.render("# Заголовок\n", vault, fileDir);
if (!probe(h, "<h1>Заголовок</h1>"))
return fail("заголовок", h);
// Жирный / курсив / зачёркнутый
QString f = p.render("**ж** *к* ~~з~~\n", vault, fileDir);
if (!probe(f, "<b>ж</b>") || !probe(f, "<i>к</i>") || !probe(f, "<s>з</s>"))
return fail("инлайн-разметка", f);
// Таблица GFM
QString t = p.render("| A | B |\n|---|---|\n| 1 | 2 |\n", vault, fileDir);
if (!probe(t, "<table") || !probe(t, "<th") || !probe(t, "<td"))
return fail("таблица", t);
// Задачи (переключаемые ссылки + галочки)
QString sk = p.render("- [ ] задача\n- [x] готова\n", vault, fileDir);
if (!probe(sk, "flora://task/") || !probe(sk, "\u2610") || !probe(sk, "\u2611"))
return fail("задачи", sk);
// Относительная ссылка на существующий файл → flora://open
QString lk = p.render("[б](b.md)\n", vault, fileDir);
if (!probe(lk, "flora://open/") || !probe(lk, ">б</a>"))
return fail("ссылка", lk);
// Вики-ссылка [[b]]
QString wk = p.render("[[b]]\n", vault, fileDir);
if (!probe(wk, "flora://open/") || !probe(wk, ">b</a>"))
return fail("вики-ссылка", wk);
// Подсветка кода python (должны появиться цветные span'ы)
QString c = p.render("```python\nprint(\"hi\")\n```\n", vault, fileDir);
if (!probe(c, "language-Python") || !probe(c, "style=\"color:"))
return fail("подсветка кода", c);
// Простой код без языка — не должен падать
p.render("```\nпросто\n```\n", vault, fileDir);
// Язык «С++» из кириллицы должен мапиться на C++ (подсветка сработает)
QString cpp = p.render("```С++\nint x = 1;\n```\n", vault, fileDir);
if (!probe(cpp, "language-C++"))
return fail("алиас С++", cpp);
// Экранирование HTML — опасные теги должны экранироваться
QString e = p.render("<script>alert(1)</script>\n", vault, fileDir);
if (probe(e, "<script>"))
return fail("экранирование script", e);
// Безопасные теги должны рендериться как HTML
QString sup = p.render("x<sup>2</sup>=4\n", vault, fileDir);
if (!probe(sup, "<sup>2</sup>"))
return fail("sup не отрендерился", sup);
if (sup.trimmed().isEmpty())
return fail("sup пустой", sup);
QString span = p.render("<span style=\"background-color:#ffeb3b\">hi</span>\n", vault, fileDir);
if (!probe(span, "<span") || !probe(span, "background-color"))
return fail("span не отрендерился", span);
// Картинка → data: или placeholder (файл невалидный PNG — data не выйдет)
QString im = p.render("![а](pic.png)\n", vault, fileDir);
if (!probe(im, "<img") && !probe(im, "изображение"))
return fail("картинка", im);
// CodeBlock отдельный компонент — parseBlocks
auto blocks = p.parseBlocks("# Hello\n\n```cpp\nint x=1;\n```\n\nText", vault, fileDir);
if (blocks.size() != 3) return fail("parseBlocks size", QString::number(blocks.size()));
if (blocks[0].toMap().value("type").toString() != "html") return fail("block 0 type", blocks[0].toMap().value("type").toString());
if (blocks[1].toMap().value("type").toString() != "code") return fail("block 1 type", blocks[1].toMap().value("type").toString());
if (blocks[1].toMap().value("language").toString() != "C++" && blocks[1].toMap().value("language").toString() != "cpp") return fail("block 1 lang", blocks[1].toMap().value("language").toString());
{
QString codeVal = blocks[1].toMap().value("codeText").toString();
if (codeVal.isEmpty()) codeVal = blocks[1].toMap().value("code").toString();
if (!codeVal.contains("int x=1;")) return fail("block 1 code", codeVal);
}
{
QString hl = blocks[1].toMap().value("highlightedText").toString();
if (hl.isEmpty()) hl = blocks[1].toMap().value("highlighted").toString();
if (hl.isEmpty()) return fail("highlighted empty");
// Debug: print highlighted
std::fprintf(stderr, "highlighted: %s\n", qPrintable(hl.left(500)));
}
if (blocks[2].toMap().value("type").toString() != "html") return fail("block 2 type");
// Test with #include
{
auto b2 = p.parseBlocks("```cpp\n#include <stdio.h>\nint main() { return 0; }\n```", vault, fileDir);
if (b2.size() != 1 || b2[0].toMap().value("type").toString() != "code") return fail("include block");
QString hl2 = b2[0].toMap().value("highlightedText").toString();
if (hl2.isEmpty()) hl2 = b2[0].toMap().value("highlighted").toString();
std::fprintf(stderr, "hl2: %s\n", qPrintable(hl2.left(800)));
if (!hl2.contains("span")) return fail("hl2 no span", hl2.left(200));
}
// Подсветка: переносы строк — явные <br/>, иначе RichText склеит в одну строку
{
auto b = p.parseBlocks("```cpp\nint a = 1;\nint b = 2;\n```\n", vault, fileDir);
if (b.size() != 1) return fail("br блок");
const QString hl = b[0].toMap().value("highlightedText").toString();
if (!hl.contains("<br/>")) return fail("highlighted без <br/>", hl.left(200));
if (hl.contains("\n")) return fail("highlighted с сырым \\n", hl.left(200));
}
// parseBlocksWithDiff: версия old vs текущее new
{
const QString oldMd = "# Заголовок\n\nСтарая строка\n\n```cpp\nint x = 1;\n```\n";
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,
// поэтому пары 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("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("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 для копирования
if (d[2].toMap().value("codeId").toString().isEmpty()) return fail("diff codeId пуст");
if (p.getCodeBlock(d[2].toMap().value("codeId").toString()) != "int x = 1;")
return fail("getCodeBlock после diff", p.getCodeBlock(d[2].toMap().value("codeId").toString()));
}
// parseBlocksWithDiff: идентичные тексты — всё same, без подсветки
{
const QString md = "# A\n\ntext\n";
auto d = p.parseBlocksWithDiff(md, md, vault, fileDir);
for (const auto &v : d) {
if (v.toMap().value("change").toString() != "same")
return fail("identical должен быть same");
if (v.toMap().value("html").toString().contains("244,63,94")
|| v.toMap().value("html").toString().contains("34,197,94"))
return fail("identical с подсветкой");
}
}
// ~~~-заборы тоже разбираются на code-блоки (единая разбивка splitMdBlocks)
{
auto b = p.parseBlocks("~~~\ncode\n~~~\n", vault, fileDir);
if (b.size() != 1 || b[0].toMap().value("type").toString() != "code")
return fail("tilde fence");
if (!b[0].toMap().value("codeText").toString().contains("code"))
return fail("tilde code");
}
// 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() != 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("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 без зелёного");
}
// parseBlocksWithDiffAsync: тот же результат, что sync, id совпадает
{
const QString oldMd = "# T\n\n```cpp\nint a = 1;\n```\n";
const QString newMd = "# T\n\n```cpp\nint a = 2;\n```\n";
QVariantList expect = p.parseBlocksWithDiff(oldMd, newMd, vault, fileDir);
const int reqId = p.parseBlocksWithDiffAsync(oldMd, newMd, vault, fileDir);
QVariantList got;
int gotId = -1;
QObject::connect(&p, &MdParser::blocksReady,
[&](int id, const QVariantList &b) { gotId = id; got = b; });
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
QObject::connect(&p, &MdParser::blocksReady, &loop, &QEventLoop::quit);
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
timer.start(10000);
loop.exec();
if (gotId != reqId) return fail("async id", QString::number(gotId));
if (got.size() != expect.size()) return fail("async размер", QString::number(got.size()));
for (int i = 0; i < got.size(); ++i) {
const QVariantMap g = got[i].toMap(), e = expect[i].toMap();
if (g.value("type") != e.value("type") || g.value("change") != e.value("change"))
return fail("async блок", QString::number(i));
if (g.value("type").toString() == "code") {
// копирование работает и после async (id свежие, код — новый)
if (!p.getCodeBlock(g.value("codeId").toString()).contains("int a = 2;"))
return fail("async copy", p.getCodeBlock(g.value("codeId").toString()));
}
}
}
// parseBlocksWithDiff: правка внутри кода — ОДИН блок с построчным diff внутри
{
const QString oldMd = "# T\n\n```cpp\nint a = 1;\nint b = 2;\n```\n";
const QString newMd = "# T\n\n```cpp\nint a = 1;\nint b = 3;\n```\n";
auto d = p.parseBlocksWithDiff(oldMd, newMd, vault, fileDir);
if (d.size() != 2) return fail("merge размер", QString::number(d.size()));
if (d[0].toMap().value("change").toString() != "same") return fail("merge[0] same");
if (d[1].toMap().value("type").toString() != "code"
|| d[1].toMap().value("change").toString() != "mod")
return fail("merge[1] должен быть code/mod", d[1].toMap().value("change").toString());
const QString hl = d[1].toMap().value("highlightedText").toString();
if (!hl.contains("244,63,94")) return fail("merge без розовой строки");
if (!hl.contains("34,197,94")) return fail("merge без зелёной строки");
// маркеры −/+ у изменённых строк (текст режется спанами подсветки, ищем маркеры)
if (!hl.contains(QChar(0x2212))) return fail("merge без маркера −");
if (!hl.contains("+ </span>")) return fail("merge без маркера +");
// копируется новое содержимое
const QString cid = d[1].toMap().value("codeId").toString();
if (cid.isEmpty()) return fail("merge codeId пуст");
if (!p.getCodeBlock(cid).contains("int b = 3;")) return fail("merge copy даёт не новое", p.getCodeBlock(cid));
}
std::printf("MDPARSER TEST OK\n");
return 0;
}