Trim desktop message link punctuation in linear time

This commit is contained in:
evgeny committed 2026-10-27 14:22:32 +01:00
1 parent f04637a257
commit 3d48024fd8
3 files changed
+44 -10

No files matched your search

+11 -4
View File
@@ -22,6 +22,10 @@ Android и Qt распознают `http://`, `https://` и `utun://` в обы
и полные адреса в исходном написании: Qt — `start/length`, Android — `start/end`,
где end исключён. Единицы обоих API — UTF-16:
`tools/chatgui/src/message_links.cpp`, Android `data/MessageLinks.kt`.
Конец диапазона Android не включён в ссылку; Qt возвращает начало и длину.
Удаление внешней пунктуации занимает линейное время: баланс трёх видов скобок
считается один раз, хвост сокращается по индексу без повторного копирования адреса.
Порт веб-адреса пустой либо десятичный от 0 до 65535; знак перед числом недопустим.
Подсветка и нажатие используют эти диапазоны. Android подставляет emoji placeholders
только в обычный текст между ссылками: emoji внутри адреса остаётся частью полного URL.
Qt использует один QTextLayout для размеров, отрисовки, выделения и координат нажатия.
@@ -38,11 +42,14 @@ Qt `QDesktopServices::openUrl`. На нажатии веб-адрес снова
Общие случаи: `tools/chatgui/tests/message_links_cases.tsv`.
Qt `test_invite_links` проверяет разбор, перенесённые веб-ссылки, смесь схем,
нажатие, drag/copy и сокращённые приглашения; `test_emoji_playback` — жесты приглашений.
Android `tests/test_message_links.kt` проверяет тот же TSV на классах из APK-сборки:
Android `MessageLinksTest` проверяет тот же TSV через Gradle, некорректные authority
и длинный хвост пунктуации. Аналогичный тест длинного хвоста есть в Qt.
Оба разбора не открывают ссылки; некорректный пользовательский текст не создаёт логов.
```bash
cd tools/chatgui-android
./build.sh noinstall
kotlinc tests/test_message_links.kt -classpath app/build/tmp/kotlin-classes/debug -d /tmp/message-links-tests.jar
kotlin -classpath /tmp/message-links-tests.jar:app/build/tmp/kotlin-classes/debug Test_message_linksKt ../chatgui/tests/message_links_cases.tsv
./gradlew :app:testDebugUnitTest --tests com.utun.chat.data.MessageLinksTest
```
`tests/test_message_links.kt` остаётся отдельным CLI-тестом того же TSV;
ему передаётся путь к файлу, а в classpath нужны собранные классы `MessageLinks`.
+17 -6
View File
@@ -12,13 +12,24 @@ QVector<MessageLink> messageLinks(const QString& text) {
const auto match = matches.next();
QString url = match.captured();
if (!url.startsWith(QStringLiteral("utun://"))) {
while (!url.isEmpty()) {
const QChar last = url.back();
const int closing = QStringLiteral(")]}").indexOf(last);
const bool unmatched = closing >= 0 && url.count(last) > url.count(QStringLiteral("([{").at(closing));
if (!QStringLiteral(".,;:!?").contains(last) && !unmatched) break;
url.chop(1);
// Один подсчёт и один проход по хвосту: повторный count() на каждой скобке даёт O(n²).
qsizetype balance[3] = {};
for (const QChar ch : url) {
const int opening = QStringLiteral("([{").indexOf(ch);
const int closing = QStringLiteral(")]}").indexOf(ch);
if (opening >= 0) balance[opening]--;
if (closing >= 0) balance[closing]++;
}
auto end = url.size();
while (end > 0) {
const QChar last = url.at(end - 1);
const int closing = QStringLiteral(")]}").indexOf(last);
const bool unmatched = closing >= 0 && balance[closing] > 0;
if (!QStringLiteral(".,;:!?").contains(last) && !unmatched) break;
if (closing >= 0) balance[closing]--;
end--;
}
url.truncate(end);
const QUrl parsed(url, QUrl::StrictMode);
if (!parsed.isValid() || parsed.host().isEmpty()) continue;
}
+16
View File
@@ -259,6 +259,22 @@ private slots:
QVERIFY(count >= 15);
}
void longMessageLinkTail() {
const QString first = QStringLiteral("https://example.com/a([0])");
const QString second = QStringLiteral("http://example.org/b");
const QString text = QString::fromUtf8("👋 ") + first + QStringLiteral(")]},.!?").repeated(20000) + ' ' + second;
QElapsedTimer timer; timer.start();
const auto links = messageLinks(text);
const auto elapsed = timer.elapsed();
qInfo() << "Message links:" << text.size() << "UTF-16 units parsed in" << elapsed << "ms";
QCOMPARE(links.size(), 2);
QCOMPARE(links.first().url, first);
QCOMPARE(links.first().start, 3);
QCOMPARE(links.last().url, second);
QCOMPARE(text.mid(links.last().start, links.last().length), second);
QVERIFY2(elapsed < 5000, "Message link parsing exceeded the UI regression guard");
}
void webLinksUsePaintedCoordinatesAndDragDoesNotOpen() {
const QString first = QStringLiteral("https://пример.рф/путь?") + QString(160, 'a') + QStringLiteral("=1#fragment");
const QString second = QStringLiteral("http://example.org/a(b)?x=2");