Support intersecting links with entities.

This commit is contained in:
John Preston 2019-06-15 19:22:09 +02:00
parent 522e66b2db
commit 91c57f2035
1 changed files with 403 additions and 241 deletions

View File

@ -22,6 +22,8 @@ namespace Ui {
namespace Text { namespace Text {
namespace { namespace {
constexpr auto kStringLinkIndexShift = uint16(0x8000);
Qt::LayoutDirection StringDirection(const QString &str, int32 from, int32 to) { Qt::LayoutDirection StringDirection(const QString &str, int32 from, int32 to) {
const ushort *p = reinterpret_cast<const ushort*>(str.unicode()) + from; const ushort *p = reinterpret_cast<const ushort*>(str.unicode()) + from;
const ushort *end = p + (to - from); const ushort *end = p + (to - from);
@ -48,6 +50,64 @@ Qt::LayoutDirection StringDirection(const QString &str, int32 from, int32 to) {
return Qt::LayoutDirectionAuto; return Qt::LayoutDirectionAuto;
} }
TextWithEntities PrepareRichFromPlain(
const QString &text,
const TextParseOptions &options) {
auto result = TextWithEntities{ text };
if (options.flags & TextParseLinks) {
TextUtilities::ParseEntities(
result,
options.flags,
(options.flags & TextParseRichText));
}
return result;
}
TextWithEntities PrepareRichFromRich(
const TextWithEntities &text,
const TextParseOptions &options) {
auto result = text;
const auto &preparsed = text.entities;
if ((options.flags & TextParseLinks) && !preparsed.isEmpty()) {
bool parseMentions = (options.flags & TextParseMentions);
bool parseHashtags = (options.flags & TextParseHashtags);
bool parseBotCommands = (options.flags & TextParseBotCommands);
bool parseMarkdown = (options.flags & TextParseMarkdown);
if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) {
int32 i = 0, l = preparsed.size();
result.entities.clear();
result.entities.reserve(l);
const QChar s = result.text.size();
for (; i < l; ++i) {
auto type = preparsed.at(i).type();
if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) ||
(type == EntityType::Hashtag && !parseHashtags) ||
(type == EntityType::Cashtag && !parseHashtags) ||
(type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities
((type == EntityType::Bold || type == EntityType::Italic || type == EntityType::Code || type == EntityType::Pre) && !parseMarkdown)) {
continue;
}
result.entities.push_back(preparsed.at(i));
}
}
}
return result;
}
QFixed ComputeStopAfter(const TextParseOptions &options, const style::TextStyle &st) {
return (options.maxw > 0 && options.maxh > 0)
? ((options.maxh / st.font->height) + 1) * options.maxw
: QFIXED_MAX;
}
// Open Sans tilde fix.
bool ComputeCheckTilde(const style::TextStyle &st) {
const auto &font = st.font;
return (font->size() * cIntRetinaFactor() == 13)
&& (font->flags() == 0)
&& (font->f.family() == qstr("Open Sans"));
}
} // namespace } // namespace
} // namespace Text } // namespace Text
} // namespace Ui } // namespace Ui
@ -194,6 +254,9 @@ public:
const TextParseOptions &options); const TextParseOptions &options);
private: private:
struct ReadyToken {
};
enum LinkDisplayStatus { enum LinkDisplayStatus {
LinkDisplayedFull, LinkDisplayedFull,
LinkDisplayedElided, LinkDisplayedElided,
@ -211,6 +274,26 @@ private:
LinkDisplayStatus displayStatus = LinkDisplayedFull; LinkDisplayStatus displayStatus = LinkDisplayedFull;
}; };
class StartedEntity {
public:
explicit StartedEntity(TextBlockFlags flags);
explicit StartedEntity(uint16 lnkIndex);
std::optional<TextBlockFlags> flags() const;
std::optional<uint16> lnkIndex() const;
private:
int _value = 0;
};
Parser(
not_null<String*> string,
TextWithEntities &&source,
const TextParseOptions &options,
ReadyToken);
void trimSourceRange();
void blockCreated(); void blockCreated();
void createBlock(int32 skipBack = 0); void createBlock(int32 skipBack = 0);
void createSkipBlock(int32 w, int32 h); void createSkipBlock(int32 w, int32 h);
@ -223,6 +306,12 @@ private:
bool readCommand(); bool readCommand();
void parseCurrentChar(); void parseCurrentChar();
void parseEmojiFromCurrent(); void parseEmojiFromCurrent();
void checkForElidedSkipBlock();
void finalize(const TextParseOptions &options);
void finishEntities();
void skipPassedEntities();
void skipBadEntities();
bool isInvalidEntity(const EntityInText &entity) const; bool isInvalidEntity(const EntityInText &entity) const;
bool isLinkEntity(const EntityInText &entity) const; bool isLinkEntity(const EntityInText &entity) const;
@ -233,18 +322,27 @@ private:
QString *outLinkText, QString *outLinkText,
LinkDisplayStatus *outDisplayStatus); LinkDisplayStatus *outDisplayStatus);
not_null<String*> _t; static ClickHandlerPtr CreateHandlerForLink(
TextWithEntities _source; const TextLinkData &link,
const QChar *_start = nullptr; const TextParseOptions &options);
const QChar *_end = nullptr;
const QChar *_ptr = nullptr;
bool _rich = false;
bool _multiline = false;
EntitiesInText::const_iterator _waitingEntity;
EntitiesInText::const_iterator _entitiesEnd;
QVector<TextLinkData> _links; const not_null<String*> _t;
QMap<const QChar*, QList<int32>> _removeFlags; const TextWithEntities _source;
const QChar * const _start = nullptr;
const QChar *_end = nullptr; // mutable, because we trim by decrementing.
const QChar *_ptr = nullptr;
const EntitiesInText::const_iterator _entitiesEnd;
EntitiesInText::const_iterator _waitingEntity;
const bool _rich = false;
const bool _multiline = false;
const QFixed _stopAfterWidth; // summary width of all added words
const bool _checkTilde = false; // do we need a special text block for tilde symbol
std::vector<TextLinkData> _links;
base::flat_map<
const QChar*,
std::vector<StartedEntity>> _startedEntities;
uint16 _maxLnkIndex = 0; uint16 _maxLnkIndex = 0;
@ -255,7 +353,6 @@ private:
int32 _blockStart = 0; // offset in result, from which current parsed block is started int32 _blockStart = 0; // offset in result, from which current parsed block is started
int32 _diacs = 0; // diac chars skipped without good char int32 _diacs = 0; // diac chars skipped without good char
QFixed _sumWidth; QFixed _sumWidth;
QFixed _stopAfterWidth; // summary width of all added words
bool _sumFinished = false; bool _sumFinished = false;
bool _newlineAwaited = false; bool _newlineAwaited = false;
@ -263,7 +360,6 @@ private:
QChar _ch; // current char (low surrogate, if current char is surrogate pair) QChar _ch; // current char (low surrogate, if current char is surrogate pair)
int32 _emojiLookback = 0; // how far behind the current ptr to look for current emoji int32 _emojiLookback = 0; // how far behind the current ptr to look for current emoji
bool _lastSkipped = false; // did we skip current char bool _lastSkipped = false; // did we skip current char
bool _checkTilde = false; // do we need a special text block for tilde symbol
}; };
@ -278,54 +374,66 @@ Parser::TextLinkData::TextLinkData(
, displayStatus(displayStatus) { , displayStatus(displayStatus) {
} }
Parser::StartedEntity::StartedEntity(TextBlockFlags flags) : _value(flags) {
Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
}
Parser::StartedEntity::StartedEntity(uint16 lnkIndex) : _value(lnkIndex) {
Expects(_value >= kStringLinkIndexShift);
}
std::optional<TextBlockFlags> Parser::StartedEntity::flags() const {
if (_value < int(kStringLinkIndexShift)) {
return TextBlockFlags(_value);
}
return std::nullopt;
}
std::optional<uint16> Parser::StartedEntity::lnkIndex() const {
if (_value >= int(kStringLinkIndexShift)) {
return uint16(_value);
}
return std::nullopt;
}
Parser::Parser( Parser::Parser(
not_null<String*> string, not_null<String*> string,
const QString &text, const QString &text,
const TextParseOptions &options) const TextParseOptions &options)
: _t(string) : Parser(
, _source { text } string,
, _rich(options.flags & TextParseRichText) PrepareRichFromPlain(text, options),
, _multiline(options.flags & TextParseMultiline) options,
, _stopAfterWidth(QFIXED_MAX) { ReadyToken()) {
if (options.flags & TextParseLinks) {
TextUtilities::ParseEntities(_source, options.flags, _rich);
}
parse(options);
} }
Parser::Parser( Parser::Parser(
not_null<String*> string, not_null<String*> string,
const TextWithEntities &textWithEntities, const TextWithEntities &textWithEntities,
const TextParseOptions &options) const TextParseOptions &options)
: Parser(
string,
PrepareRichFromRich(textWithEntities, options),
options,
ReadyToken()) {
}
Parser::Parser(
not_null<String*> string,
TextWithEntities &&source,
const TextParseOptions &options,
ReadyToken)
: _t(string) : _t(string)
, _source(textWithEntities) , _source(std::move(source))
, _start(_source.text.constData())
, _end(_start + _source.text.size())
, _ptr(_start)
, _entitiesEnd(_source.entities.end())
, _waitingEntity(_source.entities.begin())
, _rich(options.flags & TextParseRichText) , _rich(options.flags & TextParseRichText)
, _multiline(options.flags & TextParseMultiline) , _multiline(options.flags & TextParseMultiline)
, _stopAfterWidth(QFIXED_MAX) { , _stopAfterWidth(ComputeStopAfter(options, *_t->_st))
auto &preparsed = textWithEntities.entities; , _checkTilde(ComputeCheckTilde(*_t->_st)) {
if ((options.flags & TextParseLinks) && !preparsed.isEmpty()) {
bool parseMentions = (options.flags & TextParseMentions);
bool parseHashtags = (options.flags & TextParseHashtags);
bool parseBotCommands = (options.flags & TextParseBotCommands);
bool parseMarkdown = (options.flags & TextParseMarkdown);
if (!parseMentions || !parseHashtags || !parseBotCommands || !parseMarkdown) {
int32 i = 0, l = preparsed.size();
_source.entities.clear();
_source.entities.reserve(l);
const QChar s = _source.text.size();
for (; i < l; ++i) {
auto type = preparsed.at(i).type();
if (((type == EntityType::Mention || type == EntityType::MentionName) && !parseMentions) ||
(type == EntityType::Hashtag && !parseHashtags) ||
(type == EntityType::Cashtag && !parseHashtags) ||
(type == EntityType::BotCommand && !parseBotCommands) || // #TODO entities
((type == EntityType::Bold || type == EntityType::Italic || type == EntityType::Code || type == EntityType::Pre) && !parseMarkdown)) {
continue;
}
_source.entities.push_back(preparsed.at(i));
}
}
}
parse(options); parse(options);
} }
@ -337,7 +445,10 @@ void Parser::blockCreated() {
} }
void Parser::createBlock(int32 skipBack) { void Parser::createBlock(int32 skipBack) {
if (_lnkIndex < 0x8000 && _lnkIndex > _maxLnkIndex) _maxLnkIndex = _lnkIndex; if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) {
_maxLnkIndex = _lnkIndex;
}
int32 len = int32(_t->_text.size()) + skipBack - _blockStart; int32 len = int32(_t->_text.size()) + skipBack - _blockStart;
if (len > 0) { if (len > 0) {
bool newline = !_emoji && (len == 1 && _t->_text.at(_blockStart) == QChar::LineFeed); bool newline = !_emoji && (len == 1 && _t->_text.at(_blockStart) == QChar::LineFeed);
@ -387,90 +498,109 @@ bool Parser::checkCommand() {
return result; return result;
} }
// Returns true if at least one entity was parsed in the current position. void Parser::finishEntities() {
bool Parser::checkEntities() { while (!_startedEntities.empty()
while (!_removeFlags.isEmpty() && (_ptr >= _removeFlags.firstKey() || _ptr >= _end)) { && (_ptr >= _startedEntities.begin()->first || _ptr >= _end)) {
const QList<int32> &removing(_removeFlags.first()); auto list = std::move(_startedEntities.begin()->second);
for (int32 i = removing.size(); i > 0;) { _startedEntities.erase(_startedEntities.begin());
int32 flag = removing.at(--i);
if (_flags & flag) { while (!list.empty()) {
if (const auto flags = list.back().flags()) {
if (_flags & (*flags)) {
createBlock(); createBlock();
_flags &= ~flag; _flags &= ~(*flags);
if (flag == TextBlockFPre if (((*flags) & TextBlockFPre)
&& !_t->_blocks.empty() && !_t->_blocks.empty()
&& _t->_blocks.back()->type() != TextBlockTNewline) { && _t->_blocks.back()->type() != TextBlockTNewline) {
_newlineAwaited = true; _newlineAwaited = true;
} }
} }
} else if (const auto lnkIndex = list.back().lnkIndex()) {
if (_lnkIndex == *lnkIndex) {
createBlock();
_lnkIndex = 0;
} }
_removeFlags.erase(_removeFlags.begin());
} }
while (_waitingEntity != _entitiesEnd && _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) { list.pop_back();
++_waitingEntity;
} }
if (_waitingEntity == _entitiesEnd || _ptr < _start + _waitingEntity->offset()) { }
}
// Returns true if at least one entity was parsed in the current position.
bool Parser::checkEntities() {
finishEntities();
skipPassedEntities();
if (_waitingEntity == _entitiesEnd
|| _ptr < _start + _waitingEntity->offset()) {
return false; return false;
} }
int32 startFlags = 0; auto flags = TextBlockFlags();
QString linkData, linkText; auto link = TextLinkData();
auto type = _waitingEntity->type(), linkType = EntityType::Invalid; const auto entityType = _waitingEntity->type();
LinkDisplayStatus linkDisplayStatus = LinkDisplayedFull; const auto entityLength = _waitingEntity->length();
if (type == EntityType::Bold) { const auto entityBegin = _start + _waitingEntity->offset();
startFlags = TextBlockFSemibold; const auto entityEnd = entityBegin + entityLength;
} else if (type == EntityType::Italic) { if (entityType == EntityType::Bold) {
startFlags = TextBlockFItalic; flags = TextBlockFSemibold;
} else if (type == EntityType::Code) { // #TODO entities } else if (entityType == EntityType::Italic) {
startFlags = TextBlockFCode; flags = TextBlockFItalic;
} else if (type == EntityType::Pre) { } else if (entityType == EntityType::Code) { // #TODO entities
startFlags = TextBlockFPre; flags = TextBlockFCode;
} else if (entityType == EntityType::Pre) {
flags = TextBlockFPre;
createBlock(); createBlock();
if (!_t->_blocks.empty() && _t->_blocks.back()->type() != TextBlockTNewline) { if (!_t->_blocks.empty() && _t->_blocks.back()->type() != TextBlockTNewline) {
createNewlineBlock(); createNewlineBlock();
} }
} else if (type == EntityType::Url } else if (entityType == EntityType::Url
|| type == EntityType::Email || entityType == EntityType::Email
|| type == EntityType::Mention || entityType == EntityType::Mention
|| type == EntityType::Hashtag || entityType == EntityType::Hashtag
|| type == EntityType::Cashtag || entityType == EntityType::Cashtag
|| type == EntityType::BotCommand) { || entityType == EntityType::BotCommand) {
linkType = type; link.type = entityType;
linkData = QString(_start + _waitingEntity->offset(), _waitingEntity->length()); link.data = QString(entityBegin, entityLength);
if (linkType == EntityType::Url) { if (link.type == EntityType::Url) {
computeLinkText(linkData, &linkText, &linkDisplayStatus); computeLinkText(link.data, &link.text, &link.displayStatus);
} else { } else {
linkText = linkData; link.text = link.data;
} }
} else if (type == EntityType::CustomUrl || type == EntityType::MentionName) { } else if (entityType == EntityType::CustomUrl
linkType = type; || entityType == EntityType::MentionName) {
linkData = _waitingEntity->data(); link.type = entityType;
linkText = QString(_start + _waitingEntity->offset(), _waitingEntity->length()); link.data = _waitingEntity->data();
link.text = QString(_start + _waitingEntity->offset(), _waitingEntity->length());
} }
if (linkType != EntityType::Invalid) { if (link.type != EntityType::Invalid) {
createBlock(); createBlock();
_links.push_back(TextLinkData(linkType, linkText, linkData, linkDisplayStatus)); _links.push_back(link);
_lnkIndex = 0x8000 + _links.size(); _lnkIndex = kStringLinkIndexShift + _links.size();
for (auto entityEnd = _start + _waitingEntity->offset() + _waitingEntity->length(); _ptr < entityEnd; ++_ptr) { _startedEntities[entityEnd].emplace_back(_lnkIndex);
parseCurrentChar(); } else if (flags) {
parseEmojiFromCurrent(); if (!(_flags & flags)) {
if (_sumFinished || _t->_text.size() >= 0x8000) break; // 32k max
}
createBlock(); createBlock();
_flags |= flags;
_lnkIndex = 0; _startedEntities[entityEnd].emplace_back(flags);
} else if (startFlags) {
if (!(_flags & startFlags)) {
createBlock();
_flags |= startFlags;
_removeFlags[_start + _waitingEntity->offset() + _waitingEntity->length()].push_front(startFlags);
} }
} }
++_waitingEntity; ++_waitingEntity;
skipBadEntities();
return true;
}
void Parser::skipPassedEntities() {
while (_waitingEntity != _entitiesEnd
&& _start + _waitingEntity->offset() + _waitingEntity->length() <= _ptr) {
++_waitingEntity;
}
}
void Parser::skipBadEntities() {
if (_links.size() >= 0x7FFF) { if (_links.size() >= 0x7FFF) {
while (_waitingEntity != _entitiesEnd while (_waitingEntity != _entitiesEnd
&& (isLinkEntity(*_waitingEntity) && (isLinkEntity(*_waitingEntity)
@ -482,7 +612,6 @@ bool Parser::checkEntities() {
++_waitingEntity; ++_waitingEntity;
} }
} }
return true;
} }
bool Parser::readSkipBlockCommand() { bool Parser::readSkipBlockCommand() {
@ -580,8 +709,8 @@ bool Parser::readCommand() {
case TextCommandLinkText: { case TextCommandLinkText: {
createBlock(); createBlock();
int32 len = _ptr->unicode(); int32 len = _ptr->unicode();
_links.push_back(TextLinkData(EntityType::CustomUrl, QString(), QString(++_ptr, len), LinkDisplayedFull)); _links.emplace_back(EntityType::CustomUrl, QString(), QString(++_ptr, len), LinkDisplayedFull);
_lnkIndex = 0x8000 + _links.size(); _lnkIndex = kStringLinkIndexShift + _links.size();
} break; } break;
case TextCommandSkipBlock: case TextCommandSkipBlock:
@ -594,30 +723,36 @@ bool Parser::readCommand() {
} }
void Parser::parseCurrentChar() { void Parser::parseCurrentChar() {
int skipBack = 0;
_ch = ((_ptr < _end) ? *_ptr : 0); _ch = ((_ptr < _end) ? *_ptr : 0);
_emojiLookback = 0; _emojiLookback = 0;
bool skip = false, isNewLine = _multiline && chIsNewline(_ch), isSpace = chIsSpace(_ch), isDiac = chIsDiac(_ch), isTilde = _checkTilde && (_ch == '~'); const auto isNewLine = _multiline && chIsNewline(_ch);
const auto isSpace = chIsSpace(_ch);
const auto isDiac = chIsDiac(_ch);
const auto isTilde = _checkTilde && (_ch == '~');
const auto skip = [&] {
if (chIsBad(_ch) || _ch.isLowSurrogate()) { if (chIsBad(_ch) || _ch.isLowSurrogate()) {
skip = true; return true;
} else if (_ch == 0xFE0F && Platform::IsMac()) { } else if (_ch == 0xFE0F && Platform::IsMac()) {
// Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :( // Some sequences like 0x0E53 0xFE0F crash OS X harfbuzz text processing :(
skip = true; return true;
} else if (isDiac) { } else if (isDiac) {
if (_lastSkipped || _emoji || ++_diacs > chMaxDiacAfterSymbol()) { if (_lastSkipped || _emoji || ++_diacs > chMaxDiacAfterSymbol()) {
skip = true; return true;
} }
} else if (_ch.isHighSurrogate()) { } else if (_ch.isHighSurrogate()) {
if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) { if (_ptr + 1 >= _end || !(_ptr + 1)->isLowSurrogate()) {
skip = true; return true;
} else { }
}
return false;
}();
if (_ch.isHighSurrogate() && !skip) {
_t->_text.push_back(_ch); _t->_text.push_back(_ch);
skipBack = -1;
++_ptr; ++_ptr;
_ch = *_ptr; _ch = *_ptr;
_emojiLookback = 1; _emojiLookback = 1;
} }
}
_lastSkipped = skip; _lastSkipped = skip;
if (skip) { if (skip) {
@ -625,12 +760,12 @@ void Parser::parseCurrentChar() {
} else { } else {
if (isTilde) { // tilde fix in OpenSans if (isTilde) { // tilde fix in OpenSans
if (!(_flags & TextBlockFTilde)) { if (!(_flags & TextBlockFTilde)) {
createBlock(skipBack); createBlock(-_emojiLookback);
_flags |= TextBlockFTilde; _flags |= TextBlockFTilde;
} }
} else { } else {
if (_flags & TextBlockFTilde) { if (_flags & TextBlockFTilde) {
createBlock(skipBack); createBlock(-_emojiLookback);
_flags &= ~TextBlockFTilde; _flags &= ~TextBlockFTilde;
} }
} }
@ -639,7 +774,9 @@ void Parser::parseCurrentChar() {
} else if (isSpace) { } else if (isSpace) {
_t->_text.push_back(QChar::Space); _t->_text.push_back(QChar::Space);
} else { } else {
if (_emoji) createBlock(skipBack); if (_emoji) {
createBlock(-_emojiLookback);
}
_t->_text.push_back(_ch); _t->_text.push_back(_ch);
} }
if (!isDiac) _diacs = 0; if (!isDiac) _diacs = 0;
@ -688,108 +825,73 @@ bool Parser::isLinkEntity(const EntityInText &entity) const {
} }
void Parser::parse(const TextParseOptions &options) { void Parser::parse(const TextParseOptions &options) {
if (options.maxw > 0 && options.maxh > 0) { skipBadEntities();
_stopAfterWidth = ((options.maxh / _t->_st->font->height) + 1) * options.maxw; trimSourceRange();
}
_start = _source.text.constData();
_end = _start + _source.text.size();
_entitiesEnd = _source.entities.cend();
_waitingEntity = _source.entities.cbegin();
while (_waitingEntity != _entitiesEnd && isInvalidEntity(*_waitingEntity)) {
++_waitingEntity;
}
const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset(
_source.entities,
_end - _start);
_ptr = _start;
while (_ptr != _end && chIsTrimmed(*_ptr, _rich) && _ptr != _start + firstMonospaceOffset) {
++_ptr;
}
while (_ptr != _end && chIsTrimmed(*(_end - 1), _rich)) {
--_end;
}
_t->_text.resize(0); _t->_text.resize(0);
_t->_text.reserve(_end - _ptr); _t->_text.reserve(_end - _ptr);
_checkTilde = (_t->_st->font->size() * cIntRetinaFactor() == 13)
&& (_t->_st->font->flags() == 0)
&& (_t->_st->font->f.family() == qstr("Open Sans")); // tilde Open Sans fix
for (; _ptr <= _end; ++_ptr) { for (; _ptr <= _end; ++_ptr) {
while (checkEntities() || (_rich && checkCommand())) { while (checkEntities() || (_rich && checkCommand())) {
} }
parseCurrentChar(); parseCurrentChar();
parseEmojiFromCurrent(); parseEmojiFromCurrent();
if (_sumFinished || _t->_text.size() >= 0x8000) break; // 32k max if (_sumFinished || _t->_text.size() >= 0x8000) {
break; // 32k max
}
} }
createBlock(); createBlock();
if (_sumFinished && _rich) { // we could've skipped the final skip block command checkForElidedSkipBlock();
finalize(options);
}
void Parser::trimSourceRange() {
const auto firstMonospaceOffset = EntityInText::FirstMonospaceOffset(
_source.entities,
_end - _start);
while (_ptr != _end && chIsTrimmed(*_ptr, _rich) && _ptr != _start + firstMonospaceOffset) {
++_ptr;
}
while (_ptr != _end && chIsTrimmed(*(_end - 1), _rich)) {
--_end;
}
}
void Parser::checkForElidedSkipBlock() {
if (!_sumFinished || !_rich) {
return;
}
// We could've skipped the final skip block command.
for (; _ptr < _end; ++_ptr) { for (; _ptr < _end; ++_ptr) {
if (*_ptr == TextCommand && readSkipBlockCommand()) { if (*_ptr == TextCommand && readSkipBlockCommand()) {
break; break;
} }
} }
} }
_removeFlags.clear();
void Parser::finalize(const TextParseOptions &options) {
_t->_links.resize(_maxLnkIndex); _t->_links.resize(_maxLnkIndex);
for (const auto &block : _t->_blocks) { for (const auto &block : _t->_blocks) {
const auto b = block.get(); const auto b = block.get();
if (b->lnkIndex() > 0x8000) { const auto shiftedIndex = b->lnkIndex();
_lnkIndex = _maxLnkIndex + (b->lnkIndex() - 0x8000); if (shiftedIndex <= kStringLinkIndexShift) {
if (_t->_links.size() < _lnkIndex) { continue;
_t->_links.resize(_lnkIndex);
auto &link = _links[_lnkIndex - _maxLnkIndex - 1];
auto handler = ClickHandlerPtr();
switch (link.type) {
case EntityType::CustomUrl: {
if (!link.data.isEmpty()) {
handler = std::make_shared<HiddenUrlClickHandler>(link.data);
} }
} break; const auto realIndex = (shiftedIndex - kStringLinkIndexShift);
case EntityType::Email: const auto index = _maxLnkIndex + realIndex;
case EntityType::Url: handler = std::make_shared<UrlClickHandler>(link.data, link.displayStatus == LinkDisplayedFull); break; b->setLnkIndex(index);
case EntityType::BotCommand: handler = std::make_shared<BotCommandClickHandler>(link.data); break; if (_t->_links.size() >= index) {
case EntityType::Hashtag: continue;
if (options.flags & TextTwitterMentions) {
handler = std::make_shared<UrlClickHandler>(qsl("https://twitter.com/hashtag/") + link.data.mid(1) + qsl("?src=hash"), true);
} else if (options.flags & TextInstagramMentions) {
handler = std::make_shared<UrlClickHandler>(qsl("https://instagram.com/explore/tags/") + link.data.mid(1) + '/', true);
} else {
handler = std::make_shared<HashtagClickHandler>(link.data);
}
break;
case EntityType::Cashtag:
handler = std::make_shared<CashtagClickHandler>(link.data);
break;
case EntityType::Mention:
if (options.flags & TextTwitterMentions) {
handler = std::make_shared<UrlClickHandler>(qsl("https://twitter.com/") + link.data.mid(1), true);
} else if (options.flags & TextInstagramMentions) {
handler = std::make_shared<UrlClickHandler>(qsl("https://instagram.com/") + link.data.mid(1) + '/', true);
} else {
handler = std::make_shared<MentionClickHandler>(link.data);
}
break;
case EntityType::MentionName: {
auto fields = TextUtilities::MentionNameDataToFields(link.data);
if (fields.userId) {
handler = std::make_shared<MentionNameClickHandler>(link.text, fields.userId, fields.accessHash);
} else {
LOG(("Bad mention name: %1").arg(link.data));
}
} break;
} }
_t->_links.resize(index);
const auto handler = CreateHandlerForLink(
_links[realIndex - 1],
options);
if (handler) { if (handler) {
_t->setLink(_lnkIndex, handler); _t->setLink(index, handler);
}
}
b->setLnkIndex(_lnkIndex);
} }
} }
_t->_links.squeeze(); _t->_links.squeeze();
@ -809,6 +911,70 @@ void Parser::computeLinkText(const QString &linkData, QString *outLinkText, Link
*outDisplayStatus = (*outLinkText == readable) ? LinkDisplayedFull : LinkDisplayedElided; *outDisplayStatus = (*outLinkText == readable) ? LinkDisplayedFull : LinkDisplayedElided;
} }
ClickHandlerPtr Parser::CreateHandlerForLink(
const TextLinkData &link,
const TextParseOptions &options) {
switch (link.type) {
case EntityType::CustomUrl:
return !link.data.isEmpty()
? std::make_shared<HiddenUrlClickHandler>(link.data)
: nullptr;
case EntityType::Email:
case EntityType::Url:
return std::make_shared<UrlClickHandler>(
link.data,
link.displayStatus == LinkDisplayedFull);
case EntityType::BotCommand:
return std::make_shared<BotCommandClickHandler>(link.data);
case EntityType::Hashtag:
if (options.flags & TextTwitterMentions) {
return std::make_shared<UrlClickHandler>(
(qsl("https://twitter.com/hashtag/")
+ link.data.mid(1)
+ qsl("?src=hash")),
true);
} else if (options.flags & TextInstagramMentions) {
return std::make_shared<UrlClickHandler>(
(qsl("https://instagram.com/explore/tags/")
+ link.data.mid(1)
+ '/'),
true);
}
return std::make_shared<HashtagClickHandler>(link.data);
case EntityType::Cashtag:
return std::make_shared<CashtagClickHandler>(link.data);
case EntityType::Mention:
if (options.flags & TextTwitterMentions) {
return std::make_shared<UrlClickHandler>(
qsl("https://twitter.com/") + link.data.mid(1),
true);
} else if (options.flags & TextInstagramMentions) {
return std::make_shared<UrlClickHandler>(
qsl("https://instagram.com/") + link.data.mid(1) + '/',
true);
}
return std::make_shared<MentionClickHandler>(link.data);
case EntityType::MentionName: {
auto fields = TextUtilities::MentionNameDataToFields(link.data);
if (fields.userId) {
return std::make_shared<MentionNameClickHandler>(
link.text,
fields.userId,
fields.accessHash);
} else {
LOG(("Bad mention name: %1").arg(link.data));
}
} break;
}
return nullptr;
}
namespace { namespace {
// COPIED FROM qtextengine.cpp AND MODIFIED // COPIED FROM qtextengine.cpp AND MODIFIED
@ -1849,7 +2015,10 @@ private:
} }
style::font applyFlags(int32 flags, const style::font &f) { style::font applyFlags(int32 flags, const style::font &f) {
style::font result = f; if (!flags) {
return f;
}
auto result = f;
if ((flags & TextBlockFPre) || (flags & TextBlockFCode)) { if ((flags & TextBlockFPre) || (flags & TextBlockFCode)) {
result = App::monofont(); result = App::monofont();
if (result->size() != f->size() || result->flags() != f->flags()) { if (result->size() != f->size() || result->flags() != f->flags()) {
@ -1874,27 +2043,20 @@ private:
} }
void eSetFont(AbstractBlock *block) { void eSetFont(AbstractBlock *block) {
style::font newFont = _t->_st->font; const auto flags = block->flags();
int flags = block->flags(); const auto usedFont = [&] {
if (flags) { if (const auto index = block->lnkIndex()) {
newFont = applyFlags(flags, _t->_st->font); return ClickHandler::showAsActive(_t->_links.at(index - 1))
} ? _t->_st->linkFontOver
if (block->lnkIndex()) { : _t->_st->linkFont;
if (ClickHandler::showAsActive(_t->_links.at(block->lnkIndex() - 1))) {
if (_t->_st->font != _t->_st->linkFontOver) {
newFont = _t->_st->linkFontOver;
}
} else {
if (_t->_st->font != _t->_st->linkFont) {
newFont = _t->_st->linkFont;
}
}
} }
return _t->_st->font;
}();
const auto newFont = applyFlags(flags, usedFont);
if (newFont != _f) { if (newFont != _f) {
if (newFont->family() == _t->_st->font->family()) { _f = (newFont->family() == _t->_st->font->family())
newFont = applyFlags(flags | newFont->flags(), _t->_st->font); ? applyFlags(flags | newFont->flags(), _t->_st->font)
} : newFont;
_f = newFont;
_e->fnt = _f->f; _e->fnt = _f->f;
_e->resetFontEngineCache(); _e->resetFontEngineCache();
} }