@@ -19,6 +19,7 @@ Copyright (c) 2014 John Preston, https://tdesktop.com
1919#include " text.h"
2020
2121#include " lang.h"
22+ #include " app.h"
2223
2324#include < private/qharfbuzz_p.h>
2425
@@ -111,8 +112,9 @@ namespace {
111112 }
112113
113114 const QRegularExpression reDomain (QString::fromUtf8(" (?<![A-Za-z\\ $0-9А-Яа-яёЁ\\ -\\ _%=])(?:([a-zA-Z]+)://)?((?:[A-Za-zА-яА-ЯёЁ0-9\\ -\\ _]+\\ .){1,5}([A-Za-zрф\\ -\\ d]{2,22}))" ));
114- const QRegularExpression reMailName (QString::fromUtf8(" [a-zA-Z\\ -_\\ .0-9]{1,256}$" ));
115- const QRegularExpression reMailStart (QString::fromUtf8(" ^[a-zA-Z\\ -_\\ .0-9]{1,256}\\ @" ));
115+ const QRegularExpression reMailName (qsl(" [a-zA-Z\\ -_\\ .0-9]{1,256}$" ));
116+ const QRegularExpression reMailStart (qsl(" ^[a-zA-Z\\ -_\\ .0-9]{1,256}\\ @" ));
117+ const QRegularExpression reHashtag (qsl(" (^|[\\ s\\ .,:;<>|'\"\\ [\\ ]\\ {\\ }`\\ ~\\ !\\ %\\ ^\\ *\\ (\\ )\\ -\\ +=\\ x10])#[A-Za-z_\\ .0-9]{4,20}([\\ s\\ .,:;<>|'\"\\ [\\ ]\\ {\\ }`\\ ~\\ !\\ %\\ ^\\ *\\ (\\ )\\ -\\ +=\\ x10]|$)" ));
116118 QSet<int32> validProtocols, validTopDomains;
117119 void initLinkSets ();
118120
@@ -298,7 +300,7 @@ class TextParser {
298300 return Qt::LayoutDirectionAuto;
299301 }
300302
301- void prepareLinks () { // support emails!
303+ void prepareLinks () { // support emails and hashtags !
302304 if (validProtocols.empty ()) {
303305 initLinkSets ();
304306 }
@@ -313,73 +315,98 @@ class TextParser {
313315 }
314316 }
315317 QRegularExpressionMatch mDomain = reDomain.match (src, offset);
316- if (!mDomain .hasMatch ()) break ;
318+ QRegularExpressionMatch mHashtag = reHashtag.match (src, offset);
319+ if (!mDomain .hasMatch () && !mHashtag .hasMatch ()) break ;
317320
318- int32 domainOffset = mDomain .capturedStart (), domainEnd = mDomain .capturedEnd ();
319- if (domainOffset > nextCmd) {
320- const QChar *after = skipCommand (srcData + nextCmd, srcData + len);
321- if (after > srcData + nextCmd && domainOffset < (after - srcData)) {
322- nextCmd = offset = after - srcData;
323- continue ;
321+ LinkRange link;
322+ int32 domainOffset = mDomain .hasMatch () ? mDomain .capturedStart () : INT_MAX ,
323+ domainEnd = mDomain .hasMatch () ? mDomain .capturedEnd () : INT_MAX ,
324+ hashtagOffset = mHashtag .hasMatch () ? mHashtag .capturedStart () : INT_MAX ,
325+ hashtagEnd = mHashtag .hasMatch () ? mHashtag .capturedEnd () : INT_MAX ;
326+ if (mHashtag .hasMatch ()) {
327+ if (!mHashtag .capturedRef (1 ).isEmpty ()) {
328+ ++hashtagOffset;
329+ }
330+ if (!mHashtag .capturedRef (2 ).isEmpty ()) {
331+ --hashtagEnd;
324332 }
325333 }
334+ if (hashtagOffset < domainOffset) {
335+ if (hashtagOffset > nextCmd) {
336+ const QChar *after = skipCommand (srcData + nextCmd, srcData + len);
337+ if (after > srcData + nextCmd && hashtagOffset < (after - srcData)) {
338+ nextCmd = offset = after - srcData;
339+ continue ;
340+ }
341+ }
342+
343+ link.from = start + hashtagOffset;
344+ link.len = start + hashtagEnd - link.from ;
345+ } else {
346+ if (domainOffset > nextCmd) {
347+ const QChar *after = skipCommand (srcData + nextCmd, srcData + len);
348+ if (after > srcData + nextCmd && domainOffset < (after - srcData)) {
349+ nextCmd = offset = after - srcData;
350+ continue ;
351+ }
352+ }
326353
327- QString protocol = mDomain .captured (1 ).toLower ();
328- QString topDomain = mDomain .captured (3 ).toLower ();
329-
330- bool isProtocolValid = protocol.isEmpty () || validProtocols.contains (hashCrc32 (protocol.constData (), protocol.size () * sizeof (QChar)));
331- bool isTopDomainValid = validTopDomains.contains (hashCrc32 (topDomain.constData (), topDomain.size () * sizeof (QChar)));
354+ QString protocol = mDomain .captured (1 ).toLower ();
355+ QString topDomain = mDomain .captured (3 ).toLower ();
332356
333- if (!isProtocolValid || !isTopDomainValid) {
334- offset = domainEnd;
335- continue ;
336- }
357+ bool isProtocolValid = protocol.isEmpty () || validProtocols.contains (hashCrc32 (protocol.constData (), protocol.size () * sizeof (QChar)));
358+ bool isTopDomainValid = validTopDomains.contains (hashCrc32 (topDomain.constData (), topDomain.size () * sizeof (QChar)));
337359
338- LinkRange link;
339- if (protocol.isEmpty () && domainOffset > offset + 1 && *(start + domainOffset - 1 ) == QChar (' @' )) {
340- QString forMailName = src.mid (offset, domainOffset - offset - 1 );
341- QRegularExpressionMatch mMailName = reMailName.match (forMailName);
342- if (mMailName .hasMatch ()) {
343- int32 mailOffset = offset + mMailName .capturedStart ();
344- if (mailOffset < offset) {
345- mailOffset = offset;
346- }
347- link.from = start + mailOffset;
348- link.len = domainEnd - mailOffset;
360+ if (!isProtocolValid || !isTopDomainValid) {
361+ offset = domainEnd;
362+ continue ;
349363 }
350- }
351- if (!link.from || !link.len ) {
352- link.from = start + domainOffset;
353-
354- QStack<const QChar*> parenth;
355- const QChar *p = start + mDomain .capturedEnd ();
356- for (; p < end; ++p) {
357- QChar ch (*p);
358- if (chIsLinkEnd (ch)) break ; // link finished
359- if (chIsAlmostLinkEnd (ch)) {
360- const QChar *endTest = p + 1 ;
361- while (endTest < end && chIsAlmostLinkEnd (*endTest)) {
362- ++endTest;
363- }
364- if (endTest >= end || chIsLinkEnd (*endTest)) {
365- break ; // link finished at p
364+
365+ if (protocol.isEmpty () && domainOffset > offset + 1 && *(start + domainOffset - 1 ) == QChar (' @' )) {
366+ QString forMailName = src.mid (offset, domainOffset - offset - 1 );
367+ QRegularExpressionMatch mMailName = reMailName.match (forMailName);
368+ if (mMailName .hasMatch ()) {
369+ int32 mailOffset = offset + mMailName .capturedStart ();
370+ if (mailOffset < offset) {
371+ mailOffset = offset;
366372 }
367- p = endTest ;
368- ch = *p ;
373+ link. from = start + mailOffset ;
374+ link. len = domainEnd - mailOffset ;
369375 }
370- if (ch == ' (' || ch == ' [' || ch == ' {' || ch == ' <' ) {
371- parenth.push (p);
372- } else if (ch == ' )' || ch == ' ]' || ch == ' }' || ch == ' >' ) {
373- if (parenth.isEmpty ()) break ;
374- const QChar *q = parenth.pop (), open (*q);
375- if ((ch == ' )' && open != ' (' ) || (ch == ' ]' && open != ' [' ) || (ch == ' }' && open != ' {' ) || (ch == ' >' && open != ' <' )) {
376- p = q;
377- break ;
376+ }
377+ if (!link.from || !link.len ) {
378+ link.from = start + domainOffset;
379+
380+ QStack<const QChar*> parenth;
381+ const QChar *p = start + mDomain .capturedEnd ();
382+ for (; p < end; ++p) {
383+ QChar ch (*p);
384+ if (chIsLinkEnd (ch)) break ; // link finished
385+ if (chIsAlmostLinkEnd (ch)) {
386+ const QChar *endTest = p + 1 ;
387+ while (endTest < end && chIsAlmostLinkEnd (*endTest)) {
388+ ++endTest;
389+ }
390+ if (endTest >= end || chIsLinkEnd (*endTest)) {
391+ break ; // link finished at p
392+ }
393+ p = endTest;
394+ ch = *p;
395+ }
396+ if (ch == ' (' || ch == ' [' || ch == ' {' || ch == ' <' ) {
397+ parenth.push (p);
398+ } else if (ch == ' )' || ch == ' ]' || ch == ' }' || ch == ' >' ) {
399+ if (parenth.isEmpty ()) break ;
400+ const QChar *q = parenth.pop (), open (*q);
401+ if ((ch == ' )' && open != ' (' ) || (ch == ' ]' && open != ' [' ) || (ch == ' }' && open != ' {' ) || (ch == ' >' && open != ' <' )) {
402+ p = q;
403+ break ;
404+ }
378405 }
379406 }
380- }
381407
382- link.len = p - link.from ;
408+ link.len = p - link.from ;
409+ }
383410 }
384411 lnkRanges.push_back (link);
385412
@@ -421,9 +448,12 @@ class TextParser {
421448 }
422449
423450 void getLinkData (const QString &original, QString &result, int32 &fullDisplayed) {
424- if (reMailStart.match (original).hasMatch ()) {
451+ if (!original.isEmpty () && original.at (0 ) == ' #' ) {
452+ result = original;
453+ fullDisplayed = -2 ; // hashtag
454+ } else if (reMailStart.match (original).hasMatch ()) {
425455 result = original;
426- fullDisplayed = -1 ;
456+ fullDisplayed = -1 ; // email
427457 } else {
428458 QUrl url (original), good (url.isValid () ? url.toEncoded () : " " );
429459 QString readable = good.isValid () ? good.toDisplayString () : original;
@@ -725,7 +755,9 @@ class TextParser {
725755 _t->_links .resize (lnkIndex);
726756 const TextLinkData &data (links[lnkIndex - maxLnkIndex - 1 ]);
727757 TextLinkPtr lnk;
728- if (data.fullDisplayed < 0 ) { // email
758+ if (data.fullDisplayed < -1 ) { // hashtag
759+ lnk = TextLinkPtr (new HashtagLink (data.url ));
760+ } else if (data.fullDisplayed < 0 ) { // email
729761 lnk = TextLinkPtr (new EmailLink (data.url ));
730762 } else {
731763 lnk = TextLinkPtr (new TextLink (data.url , data.fullDisplayed > 0 ));
@@ -755,7 +787,7 @@ class TextParser {
755787 TextLinkData (const QString &url = QString(), int32 fullDisplayed = 1 ) : url(url), fullDisplayed(fullDisplayed) {
756788 }
757789 QString url;
758- int32 fullDisplayed; // < 0 - email
790+ int32 fullDisplayed; // -2 - hashtag, -1 - email
759791 };
760792 typedef QVector<TextLinkData> TextLinks;
761793 TextLinks links;
@@ -874,6 +906,12 @@ namespace {
874906
875907}
876908
909+ void HashtagLink::onClick (Qt::MouseButton button) const {
910+ if (button == Qt::LeftButton || button == Qt::MiddleButton) {
911+ App::searchByHashtag (_tag);
912+ }
913+ }
914+
877915class TextPainter {
878916public:
879917
0 commit comments