Trim desktop message link punctuation in linear time
This commit is contained in:
1 parent
f04637a257
commit
3d48024fd8
3 files changed
+44
-10
No files matched your search
+11
-4
@@ -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`.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user