1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "chat_helpers/message_field.h"
9 
10 #include "history/history_widget.h"
11 #include "history/history.h" // History::session
12 #include "history/history_item.h" // HistoryItem::originalText
13 #include "base/qthelp_regex.h"
14 #include "base/qthelp_url.h"
15 #include "base/event_filter.h"
16 #include "boxes/abstract_box.h"
17 #include "core/shortcuts.h"
18 #include "core/application.h"
19 #include "core/core_settings.h"
20 #include "ui/wrap/vertical_layout.h"
21 #include "ui/widgets/popup_menu.h"
22 #include "ui/ui_utility.h"
23 #include "data/data_session.h"
24 #include "data/data_user.h"
25 #include "chat_helpers/emoji_suggestions_widget.h"
26 #include "window/window_session_controller.h"
27 #include "lang/lang_keys.h"
28 #include "mainwindow.h"
29 #include "main/main_session.h"
30 #include "styles/style_layers.h"
31 #include "styles/style_boxes.h"
32 #include "styles/style_chat.h"
33 #include "base/qt_adapters.h"
34 
35 #include <QtCore/QMimeData>
36 #include <QtCore/QStack>
37 #include <QtGui/QGuiApplication>
38 #include <QtGui/QTextBlock>
39 #include <QtGui/QClipboard>
40 #include <QtWidgets/QApplication>
41 
42 namespace {
43 
44 using namespace Ui::Text;
45 
46 using EditLinkAction = Ui::InputField::EditLinkAction;
47 using EditLinkSelection = Ui::InputField::EditLinkSelection;
48 
49 constexpr auto kParseLinksTimeout = crl::time(1000);
50 
51 // For mention tags save and validate userId, ignore tags for different userId.
52 class FieldTagMimeProcessor : public Ui::InputField::TagMimeProcessor {
53 public:
54 	explicit FieldTagMimeProcessor(
55 		not_null<Window::SessionController*> controller);
56 
57 	QString tagFromMimeTag(const QString &mimeTag) override;
58 
59 private:
60 	const not_null<Window::SessionController*> _controller;
61 
62 };
63 
64 class EditLinkBox : public Ui::BoxContent {
65 public:
66 	EditLinkBox(
67 		QWidget*,
68 		not_null<Window::SessionController*> controller,
69 		const QString &text,
70 		const QString &link,
71 		Fn<void(QString, QString)> callback);
72 
73 	void setInnerFocus() override;
74 
75 protected:
76 	void prepare() override;
77 
78 private:
79 	const not_null<Window::SessionController*> _controller;
80 	QString _startText;
81 	QString _startLink;
82 	Fn<void(QString, QString)> _callback;
83 	Fn<void()> _setInnerFocus;
84 
85 };
86 
FieldTagMimeProcessor(not_null<Window::SessionController * > controller)87 FieldTagMimeProcessor::FieldTagMimeProcessor(
88 	not_null<Window::SessionController*> controller)
89 : _controller(controller) {
90 }
91 
tagFromMimeTag(const QString & mimeTag)92 QString FieldTagMimeProcessor::tagFromMimeTag(const QString &mimeTag) {
93 	if (TextUtilities::IsMentionLink(mimeTag)) {
94 		const auto userId = _controller->session().userId();
95 		auto match = QRegularExpression(":(\\d+)$").match(mimeTag);
96 		if (!match.hasMatch()
97 			|| match.capturedView(1).toULongLong() != userId.bare) {
98 			return QString();
99 		}
100 		return mimeTag.mid(0, mimeTag.size() - match.capturedLength());
101 	}
102 	return mimeTag;
103 }
104 
105 //bool ValidateUrl(const QString &value) {
106 //	const auto match = qthelp::RegExpDomain().match(value);
107 //	if (!match.hasMatch() || match.capturedStart() != 0) {
108 //		return false;
109 //	}
110 //	const auto protocolMatch = RegExpProtocol().match(value);
111 //	return protocolMatch.hasMatch()
112 //		&& IsGoodProtocol(protocolMatch.captured(1));
113 //}
114 
EditLinkBox(QWidget *,not_null<Window::SessionController * > controller,const QString & text,const QString & link,Fn<void (QString,QString)> callback)115 EditLinkBox::EditLinkBox(
116 	QWidget*,
117 	not_null<Window::SessionController*> controller,
118 	const QString &text,
119 	const QString &link,
120 	Fn<void(QString, QString)> callback)
121 : _controller(controller)
122 , _startText(text)
123 , _startLink(link)
124 , _callback(std::move(callback)) {
125 	Expects(_callback != nullptr);
126 }
127 
setInnerFocus()128 void EditLinkBox::setInnerFocus() {
129 	Expects(_setInnerFocus != nullptr);
130 
131 	_setInnerFocus();
132 }
133 
prepare()134 void EditLinkBox::prepare() {
135 	const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
136 
137 	const auto session = &_controller->session();
138 	const auto text = content->add(
139 		object_ptr<Ui::InputField>(
140 			content,
141 			st::defaultInputField,
142 			tr::lng_formatting_link_text(),
143 			_startText),
144 		st::markdownLinkFieldPadding);
145 	text->setInstantReplaces(Ui::InstantReplaces::Default());
146 	text->setInstantReplacesEnabled(
147 		Core::App().settings().replaceEmojiValue());
148 	Ui::Emoji::SuggestionsController::Init(
149 		getDelegate()->outerContainer(),
150 		text,
151 		session);
152 	InitSpellchecker(_controller, text);
153 
154 	const auto placeholder = content->add(
155 		object_ptr<Ui::RpWidget>(content),
156 		st::markdownLinkFieldPadding);
157 	placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
158 	const auto url = Ui::AttachParentChild(
159 		content,
160 		object_ptr<Ui::MaskedInputField>(
161 			content,
162 			st::defaultInputField,
163 			tr::lng_formatting_link_url(),
164 			_startLink.trimmed()));
165 	url->heightValue(
166 	) | rpl::start_with_next([placeholder](int height) {
167 		placeholder->resize(placeholder->width(), height);
168 	}, placeholder->lifetime());
169 	placeholder->widthValue(
170 	) | rpl::start_with_next([=](int width) {
171 		url->resize(width, url->height());
172 	}, placeholder->lifetime());
173 	url->move(placeholder->pos());
174 
175 	const auto submit = [=] {
176 		const auto linkText = text->getLastText();
177 		const auto linkUrl = qthelp::validate_url(url->getLastText());
178 		if (linkText.isEmpty()) {
179 			text->showError();
180 			return;
181 		} else if (linkUrl.isEmpty()) {
182 			url->showError();
183 			return;
184 		}
185 		const auto weak = Ui::MakeWeak(this);
186 		_callback(linkText, linkUrl);
187 		if (weak) {
188 			closeBox();
189 		}
190 	};
191 
192 	connect(text, &Ui::InputField::submitted, [=] {
193 		url->setFocusFast();
194 	});
195 	connect(url, &Ui::MaskedInputField::submitted, [=] {
196 		if (text->getLastText().isEmpty()) {
197 			text->setFocusFast();
198 		} else {
199 			submit();
200 		}
201 	});
202 
203 	setTitle(url->getLastText().isEmpty()
204 		? tr::lng_formatting_link_create_title()
205 		: tr::lng_formatting_link_edit_title());
206 
207 	addButton(tr::lng_formatting_link_create(), submit);
208 	addButton(tr::lng_cancel(), [=] { closeBox(); });
209 
210 	content->resizeToWidth(st::boxWidth);
211 	content->moveToLeft(0, 0);
212 	setDimensions(st::boxWidth, content->height());
213 
214 	_setInnerFocus = [=] {
215 		if (_startText.isEmpty()) {
216 			text->setFocusFast();
217 		} else {
218 			url->setFocusFast();
219 		}
220 	};
221 }
222 
StripSupportHashtag(TextWithEntities && text)223 TextWithEntities StripSupportHashtag(TextWithEntities &&text) {
224 	static const auto expression = QRegularExpression(
225 		qsl("\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"),
226 		QRegularExpression::CaseInsensitiveOption);
227 	const auto match = expression.match(text.text);
228 	if (!match.hasMatch()) {
229 		return std::move(text);
230 	}
231 	text.text.chop(match.capturedLength());
232 	const auto length = text.text.size();
233 	if (!length) {
234 		return TextWithEntities();
235 	}
236 	for (auto i = text.entities.begin(); i != text.entities.end();) {
237 		auto &entity = *i;
238 		if (entity.offset() >= length) {
239 			i = text.entities.erase(i);
240 			continue;
241 		} else if (entity.offset() + entity.length() > length) {
242 			entity.shrinkFromRight(length - entity.offset());
243 		}
244 		++i;
245 	}
246 	return std::move(text);
247 }
248 
249 } // namespace
250 
PrepareMentionTag(not_null<UserData * > user)251 QString PrepareMentionTag(not_null<UserData*> user) {
252 	return TextUtilities::kMentionTagStart
253 		+ QString::number(user->id.value)
254 		+ '.'
255 		+ QString::number(user->accessHash());
256 }
257 
PrepareEditText(not_null<HistoryItem * > item)258 TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
259 	const auto original = item->history()->session().supportMode()
260 		? StripSupportHashtag(item->originalText())
261 		: item->originalText();
262 	return TextWithTags{
263 		original.text,
264 		TextUtilities::ConvertEntitiesToTextTags(original.entities)
265 	};
266 }
267 
268 Fn<bool(
269 	Ui::InputField::EditLinkSelection selection,
270 	QString text,
271 	QString link,
DefaultEditLinkCallback(not_null<Window::SessionController * > controller,not_null<Ui::InputField * > field)272 	EditLinkAction action)> DefaultEditLinkCallback(
273 		not_null<Window::SessionController*> controller,
274 		not_null<Ui::InputField*> field) {
275 	const auto weak = Ui::MakeWeak(field);
276 	return [=](
277 			EditLinkSelection selection,
278 			QString text,
279 			QString link,
280 			EditLinkAction action) {
281 		if (action == EditLinkAction::Check) {
282 			return Ui::InputField::IsValidMarkdownLink(link)
283 				&& !TextUtilities::IsMentionLink(link);
284 		}
285 		controller->show(Box<EditLinkBox>(controller, text, link, [=](
286 				const QString &text,
287 				const QString &link) {
288 			if (const auto strong = weak.data()) {
289 				strong->commitMarkdownLinkEdit(selection, text, link);
290 			}
291 		}), Ui::LayerOption::KeepOther);
292 		return true;
293 	};
294 }
295 
InitMessageField(not_null<Window::SessionController * > controller,not_null<Ui::InputField * > field)296 void InitMessageField(
297 		not_null<Window::SessionController*> controller,
298 		not_null<Ui::InputField*> field) {
299 	field->setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding);
300 	field->setMaxHeight(st::historyComposeFieldMaxHeight);
301 
302 	field->setTagMimeProcessor(
303 		std::make_unique<FieldTagMimeProcessor>(controller));
304 
305 	field->document()->setDocumentMargin(4.);
306 	field->setAdditionalMargin(style::ConvertScale(4) - 4);
307 
308 	field->customTab(true);
309 	field->setInstantReplaces(Ui::InstantReplaces::Default());
310 	field->setInstantReplacesEnabled(
311 		Core::App().settings().replaceEmojiValue());
312 	field->setMarkdownReplacesEnabled(rpl::single(true));
313 	field->setEditLinkCallback(DefaultEditLinkCallback(controller, field));
314 }
315 
InitSpellchecker(not_null<Window::SessionController * > controller,not_null<Ui::InputField * > field)316 void InitSpellchecker(
317 		not_null<Window::SessionController*> controller,
318 		not_null<Ui::InputField*> field) {
319 #ifndef TDESKTOP_DISABLE_SPELLCHECK
320 	const auto s = Ui::CreateChild<Spellchecker::SpellingHighlighter>(
321 		field.get(),
322 		Core::App().settings().spellcheckerEnabledValue(),
323 		Spellchecker::SpellingHighlighter::CustomContextMenuItem{
324 			tr::lng_settings_manage_dictionaries(tr::now),
325 			[=] {
326 				controller->show(Box<Ui::ManageDictionariesBox>(controller));
327 			}
328 		});
329 	field->setExtendedContextMenu(s->contextMenuCreated());
330 #endif // TDESKTOP_DISABLE_SPELLCHECK
331 }
332 
HasSendText(not_null<const Ui::InputField * > field)333 bool HasSendText(not_null<const Ui::InputField*> field) {
334 	const auto &text = field->getTextWithTags().text;
335 	for (const auto &ch : text) {
336 		const auto code = ch.unicode();
337 		if (code != ' '
338 			&& code != '\n'
339 			&& code != '\r'
340 			&& !IsReplacedBySpace(code)) {
341 			return true;
342 		}
343 	}
344 	return false;
345 }
346 
ParseInlineBotQuery(not_null<Main::Session * > session,not_null<const Ui::InputField * > field)347 InlineBotQuery ParseInlineBotQuery(
348 		not_null<Main::Session*> session,
349 		not_null<const Ui::InputField*> field) {
350 	auto result = InlineBotQuery();
351 
352 	const auto &full = field->getTextWithTags();
353 	const auto &text = full.text;
354 	const auto textLength = text.size();
355 
356 	auto inlineUsernameStart = 1;
357 	auto inlineUsernameLength = 0;
358 	if (textLength > 2 && text[0] == '@' && text[1].isLetter()) {
359 		inlineUsernameLength = 1;
360 		for (auto i = inlineUsernameStart + 1; i != textLength; ++i) {
361 			const auto ch = text[i];
362 			if (ch.isLetterOrNumber() || ch.unicode() == '_') {
363 				++inlineUsernameLength;
364 				continue;
365 			} else if (!ch.isSpace()) {
366 				inlineUsernameLength = 0;
367 			}
368 			break;
369 		}
370 		auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength;
371 		auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength);
372 		auto validInlineUsername = false;
373 		if (inlineUsernameEqualsText) {
374 			validInlineUsername = text.endsWith(qstr("bot"));
375 		} else if (inlineUsernameEnd < textLength && inlineUsernameLength) {
376 			validInlineUsername = text[inlineUsernameEnd].isSpace();
377 		}
378 		if (validInlineUsername) {
379 			if (!full.tags.isEmpty()
380 				&& (full.tags.front().offset
381 					< inlineUsernameStart + inlineUsernameLength)) {
382 				return InlineBotQuery();
383 			}
384 			auto username = base::StringViewMid(text, inlineUsernameStart, inlineUsernameLength);
385 			if (username != result.username) {
386 				result.username = username.toString();
387 				if (const auto peer = session->data().peerByUsername(result.username)) {
388 					if (const auto user = peer->asUser()) {
389 						result.bot = peer->asUser();
390 					} else {
391 						result.bot = nullptr;
392 					}
393 					result.lookingUpBot = false;
394 				} else {
395 					result.bot = nullptr;
396 					result.lookingUpBot = true;
397 				}
398 			}
399 			if (result.lookingUpBot) {
400 				result.query = QString();
401 				return result;
402 			} else if (result.bot
403 				&& (!result.bot->isBot()
404 					|| result.bot->botInfo->inlinePlaceholder.isEmpty())) {
405 				result.bot = nullptr;
406 			} else {
407 				result.query = inlineUsernameEqualsText
408 					? QString()
409 					: text.mid(inlineUsernameEnd + 1);
410 				return result;
411 			}
412 		} else {
413 			inlineUsernameLength = 0;
414 		}
415 	}
416 	if (inlineUsernameLength < 3) {
417 		result.bot = nullptr;
418 		result.username = QString();
419 	}
420 	result.query = QString();
421 	return result;
422 }
423 
ParseMentionHashtagBotCommandQuery(not_null<const Ui::InputField * > field)424 AutocompleteQuery ParseMentionHashtagBotCommandQuery(
425 		not_null<const Ui::InputField*> field) {
426 	auto result = AutocompleteQuery();
427 
428 	const auto cursor = field->textCursor();
429 	if (cursor.hasSelection()) {
430 		return result;
431 	}
432 
433 	const auto position = cursor.position();
434 	const auto document = field->document();
435 	const auto block = document->findBlock(position);
436 	for (auto item = block.begin(); !item.atEnd(); ++item) {
437 		const auto fragment = item.fragment();
438 		if (!fragment.isValid()) {
439 			continue;
440 		}
441 
442 		const auto fragmentPosition = fragment.position();
443 		const auto fragmentEnd = fragmentPosition + fragment.length();
444 		if (fragmentPosition >= position || fragmentEnd < position) {
445 			continue;
446 		}
447 
448 		const auto format = fragment.charFormat();
449 		if (format.isImageFormat()) {
450 			continue;
451 		}
452 
453 		bool mentionInCommand = false;
454 		const auto text = fragment.text();
455 		for (auto i = position - fragmentPosition; i != 0; --i) {
456 			if (text[i - 1] == '@') {
457 				if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) {
458 					result.fromStart = (i == 1) && (fragmentPosition == 0);
459 					result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
460 				} else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) {
461 					mentionInCommand = true;
462 					--i;
463 					continue;
464 				}
465 				return result;
466 			} else if (text[i - 1] == '#') {
467 				if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) {
468 					result.fromStart = (i == 1) && (fragmentPosition == 0);
469 					result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
470 				}
471 				return result;
472 			} else if (text[i - 1] == '/') {
473 				if (i < 2) {
474 					result.fromStart = (i == 1) && (fragmentPosition == 0);
475 					result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
476 				}
477 				return result;
478 			}
479 			if (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) {
480 				break;
481 			}
482 			if (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') {
483 				break;
484 			}
485 		}
486 		break;
487 	}
488 	return result;
489 }
490 
MessageLinksParser(not_null<Ui::InputField * > field)491 MessageLinksParser::MessageLinksParser(not_null<Ui::InputField*> field)
492 : _field(field)
493 , _timer([=] { parse(); }) {
__anon394992770d02null494 	_connection = QObject::connect(_field, &Ui::InputField::changed, [=] {
495 		const auto length = _field->getTextWithTags().text.size();
496 		const auto timeout = (std::abs(length - _lastLength) > 2)
497 			? 0
498 			: kParseLinksTimeout;
499 		if (!_timer.isActive() || timeout < _timer.remainingTime()) {
500 			_timer.callOnce(timeout);
501 		}
502 		_lastLength = length;
503 	});
504 	_field->installEventFilter(this);
505 }
506 
parseNow()507 void MessageLinksParser::parseNow() {
508 	_timer.cancel();
509 	parse();
510 }
511 
eventFilter(QObject * object,QEvent * event)512 bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
513 	if (object == _field) {
514 		if (event->type() == QEvent::KeyPress) {
515 			const auto text = static_cast<QKeyEvent*>(event)->text();
516 			if (!text.isEmpty() && text.size() < 3) {
517 				const auto ch = text[0];
518 				if (false
519 					|| ch == '\n'
520 					|| ch == '\r'
521 					|| ch.isSpace()
522 					|| ch == QChar::LineSeparator) {
523 					_timer.callOnce(0);
524 				}
525 			}
526 		} else if (event->type() == QEvent::Drop) {
527 			_timer.callOnce(0);
528 		}
529 	}
530 	return QObject::eventFilter(object, event);
531 }
532 
list() const533 const rpl::variable<QStringList> &MessageLinksParser::list() const {
534 	return _list;
535 }
536 
parse()537 void MessageLinksParser::parse() {
538 	const auto &textWithTags = _field->getTextWithTags();
539 	const auto &text = textWithTags.text;
540 	const auto &tags = textWithTags.tags;
541 	const auto &markdownTags = _field->getMarkdownTags();
542 	if (text.isEmpty()) {
543 		_list = QStringList();
544 		return;
545 	}
546 	const auto tagCanIntersectWithLink = [](const QString &tag) {
547 		return (tag == Ui::InputField::kTagBold)
548 			|| (tag == Ui::InputField::kTagItalic)
549 			|| (tag == Ui::InputField::kTagUnderline)
550 			|| (tag == Ui::InputField::kTagStrikeOut);
551 	};
552 
553 	auto ranges = QVector<LinkRange>();
554 
555 	auto tag = tags.begin();
556 	const auto tagsEnd = tags.end();
557 	const auto processTag = [&] {
558 		Expects(tag != tagsEnd);
559 
560 		if (Ui::InputField::IsValidMarkdownLink(tag->id)
561 			&& !TextUtilities::IsMentionLink(tag->id)) {
562 			ranges.push_back({ tag->offset, tag->length, tag->id });
563 		}
564 		++tag;
565 	};
566 	const auto processTagsBefore = [&](int offset) {
567 		while (tag != tagsEnd
568 			&& (tag->offset + tag->length <= offset
569 				|| tagCanIntersectWithLink(tag->id))) {
570 			processTag();
571 		}
572 	};
573 	const auto hasTagsIntersection = [&](int till) {
574 		if (tag == tagsEnd || tag->offset >= till) {
575 			return false;
576 		}
577 		while (tag != tagsEnd && tag->offset < till) {
578 			processTag();
579 		}
580 		return true;
581 	};
582 
583 	auto markdownTag = markdownTags.begin();
584 	const auto markdownTagsEnd = markdownTags.end();
585 	const auto markdownTagsAllow = [&](int from, int length) {
586 		while (markdownTag != markdownTagsEnd
587 			&& (markdownTag->adjustedStart
588 				+ markdownTag->adjustedLength <= from
589 				|| !markdownTag->closed
590 				|| tagCanIntersectWithLink(markdownTag->tag))) {
591 			++markdownTag;
592 		}
593 		if (markdownTag == markdownTagsEnd
594 			|| markdownTag->adjustedStart >= from + length) {
595 			return true;
596 		}
597 		// Ignore http-links that are completely inside some tags.
598 		// This will allow sending http://test.com/__test__/test correctly.
599 		return (markdownTag->adjustedStart > from)
600 			|| (markdownTag->adjustedStart
601 				+ markdownTag->adjustedLength < from + length);
602 	};
603 
604 	const auto len = text.size();
605 	const QChar *start = text.unicode(), *end = start + text.size();
606 	for (auto offset = 0, matchOffset = offset; offset < len;) {
607 		auto m = qthelp::RegExpDomain().match(text, matchOffset);
608 		if (!m.hasMatch()) break;
609 
610 		auto domainOffset = m.capturedStart();
611 
612 		auto protocol = m.captured(1).toLower();
613 		auto topDomain = m.captured(3).toLower();
614 		auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol);
615 		auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain);
616 
617 		if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {
618 			auto forMailName = text.mid(offset, domainOffset - offset - 1);
619 			auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName);
620 			if (mMailName.hasMatch()) {
621 				offset = matchOffset = m.capturedEnd();
622 				continue;
623 			}
624 		}
625 		if (!isProtocolValid || !isTopDomainValid) {
626 			offset = matchOffset = m.capturedEnd();
627 			continue;
628 		}
629 
630 		QStack<const QChar*> parenth;
631 		const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;
632 		for (; p < end; ++p) {
633 			QChar ch(*p);
634 			if (IsLinkEnd(ch)) {
635 				break; // link finished
636 			} else if (IsAlmostLinkEnd(ch)) {
637 				const QChar *endTest = p + 1;
638 				while (endTest < end && IsAlmostLinkEnd(*endTest)) {
639 					++endTest;
640 				}
641 				if (endTest >= end || IsLinkEnd(*endTest)) {
642 					break; // link finished at p
643 				}
644 				p = endTest;
645 				ch = *p;
646 			}
647 			if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
648 				parenth.push(p);
649 			} else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {
650 				if (parenth.isEmpty()) break;
651 				const QChar *q = parenth.pop(), open(*q);
652 				if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {
653 					p = q;
654 					break;
655 				}
656 			}
657 		}
658 		if (p > domainEnd) { // check, that domain ended
659 			if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {
660 				matchOffset = domainEnd - start;
661 				continue;
662 			}
663 		}
664 		const auto range = LinkRange {
665 			int(domainOffset),
666 			static_cast<int>(p - start - domainOffset),
667 			QString()
668 		};
669 		processTagsBefore(domainOffset);
670 		if (!hasTagsIntersection(range.start + range.length)) {
671 			if (markdownTagsAllow(range.start, range.length)) {
672 				ranges.push_back(range);
673 			}
674 		}
675 		offset = matchOffset = p - start;
676 	}
677 	processTagsBefore(QFIXED_MAX);
678 
679 	apply(text, ranges);
680 }
681 
apply(const QString & text,const QVector<LinkRange> & ranges)682 void MessageLinksParser::apply(
683 		const QString &text,
684 		const QVector<LinkRange> &ranges) {
685 	const auto count = int(ranges.size());
686 	const auto current = _list.current();
687 	const auto computeLink = [&](const LinkRange &range) {
688 		return range.custom.isEmpty()
689 			? base::StringViewMid(text, range.start, range.length)
690 			: QStringView(range.custom);
691 	};
692 	const auto changed = [&] {
693 		if (current.size() != count) {
694 			return true;
695 		}
696 		for (auto i = 0; i != count; ++i) {
697 			if (computeLink(ranges[i]) != current[i]) {
698 				return true;
699 			}
700 		}
701 		return false;
702 	}();
703 	if (!changed) {
704 		return;
705 	}
706 	auto parsed = QStringList();
707 	parsed.reserve(count);
708 	for (const auto &range : ranges) {
709 		parsed.push_back(computeLink(range).toString());
710 	}
711 	_list = std::move(parsed);
712 }
713