Autolink.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. <?php
  2. /**
  3. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  4. * @author Nick Pope <nick@nickpope.me.uk>
  5. * @copyright Copyright © 2010, Mike Cochrane, Nick Pope
  6. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
  7. */
  8. namespace App\Util\Lexer;
  9. use Illuminate\Support\Str;
  10. use App\Services\AutolinkService;
  11. /**
  12. * Twitter Autolink Class.
  13. *
  14. * Parses tweets and generates HTML anchor tags around URLs, usernames,
  15. * username/list pairs and hashtags.
  16. *
  17. * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
  18. * is based on code by {@link http://github.com/mzsanford Matt Sanford} and
  19. * heavily modified by {@link http://github.com/ngnpope Nick Pope}.
  20. *
  21. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  22. * @author Nick Pope <nick@nickpope.me.uk>
  23. * @copyright Copyright © 2010, Mike Cochrane, Nick Pope
  24. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
  25. */
  26. class Autolink extends Regex
  27. {
  28. /**
  29. * CSS class for auto-linked URLs.
  30. *
  31. * @var string
  32. */
  33. protected $class_url = '';
  34. /**
  35. * CSS class for auto-linked username URLs.
  36. *
  37. * @var string
  38. */
  39. protected $class_user = 'u-url mention';
  40. /**
  41. * CSS class for auto-linked list URLs.
  42. *
  43. * @var string
  44. */
  45. protected $class_list = 'u-url list-slug';
  46. /**
  47. * CSS class for auto-linked hashtag URLs.
  48. *
  49. * @var string
  50. */
  51. protected $class_hash = 'u-url hashtag';
  52. /**
  53. * CSS class for auto-linked cashtag URLs.
  54. *
  55. * @var string
  56. */
  57. protected $class_cash = 'u-url cashtag';
  58. /**
  59. * URL base for username links (the username without the @ will be appended).
  60. *
  61. * @var string
  62. */
  63. protected $url_base_user = null;
  64. /**
  65. * URL base for list links (the username/list without the @ will be appended).
  66. *
  67. * @var string
  68. */
  69. protected $url_base_list = null;
  70. /**
  71. * URL base for hashtag links (the hashtag without the # will be appended).
  72. *
  73. * @var string
  74. */
  75. protected $url_base_hash = null;
  76. /**
  77. * URL base for cashtag links (the hashtag without the $ will be appended).
  78. *
  79. * @var string
  80. */
  81. protected $url_base_cash = null;
  82. /**
  83. * Whether to include the value 'nofollow' in the 'rel' attribute.
  84. *
  85. * @var bool
  86. */
  87. protected $nofollow = true;
  88. /**
  89. * Whether to include the value 'noopener' in the 'rel' attribute.
  90. *
  91. * @var bool
  92. */
  93. protected $noopener = true;
  94. /**
  95. * Whether to include the value 'external' in the 'rel' attribute.
  96. *
  97. * Often this is used to be matched on in JavaScript for dynamically adding
  98. * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
  99. * been undeprecated and thus the 'target' attribute can be used. If this is
  100. * set to false then the 'target' attribute will be output.
  101. *
  102. * @var bool
  103. */
  104. protected $external = true;
  105. /**
  106. * The scope to open the link in.
  107. *
  108. * Support for the 'target' attribute was deprecated in HTML 4.01 but has
  109. * since been reinstated in HTML 5. To output the 'target' attribute you
  110. * must disable the adding of the string 'external' to the 'rel' attribute.
  111. *
  112. * @var string
  113. */
  114. protected $target = '_blank';
  115. /**
  116. * attribute for invisible span tag.
  117. *
  118. * @var string
  119. */
  120. protected $invisibleTagAttrs = "style='position:absolute;left:-9999px;'";
  121. /**
  122. * @var Extractor
  123. */
  124. protected $extractor = null;
  125. /**
  126. * @var autolinkActiveUsersOnly
  127. */
  128. protected $autolinkActiveUsersOnly = false;
  129. /**
  130. * Provides fluent method chaining.
  131. *
  132. * @param string $tweet The tweet to be converted.
  133. * @param bool $full_encode Whether to encode all special characters.
  134. *
  135. * @see __construct()
  136. *
  137. * @return Autolink
  138. */
  139. public static function create($tweet = null, $full_encode = false)
  140. {
  141. return new static($tweet, $full_encode);
  142. }
  143. /**
  144. * Reads in a tweet to be parsed and converted to contain links.
  145. *
  146. * As the intent is to produce links and output the modified tweet to the
  147. * user, we take this opportunity to ensure that we escape user input.
  148. *
  149. * @see htmlspecialchars()
  150. *
  151. * @param string $tweet The tweet to be converted.
  152. * @param bool $escape Whether to escape the tweet (default: true).
  153. * @param bool $full_encode Whether to encode all special characters.
  154. */
  155. public function __construct($tweet = null, $escape = true, $full_encode = false)
  156. {
  157. if ($escape && !empty($tweet)) {
  158. if ($full_encode) {
  159. parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
  160. } else {
  161. parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
  162. }
  163. } else {
  164. parent::__construct($tweet);
  165. }
  166. $this->extractor = Extractor::create();
  167. $this->url_base_user = config('app.url').'/';
  168. $this->url_base_list = config('app.url').'/';
  169. $this->url_base_hash = config('app.url').'/discover/tags/';
  170. $this->url_base_cash = config('app.url').'/search?q=%24';
  171. }
  172. public function setBaseUserPath($path = '/')
  173. {
  174. $this->url_base_user = config('app.url') . $path;
  175. $this->target = null;
  176. $this->external = null;
  177. return $this;
  178. }
  179. public function setBaseHashPath($path = '/discover/tags/')
  180. {
  181. $this->url_base_hash = config('app.url') . $path;
  182. $this->target = null;
  183. $this->external = null;
  184. return $this;
  185. }
  186. public function setAutolinkActiveUsersOnly($active)
  187. {
  188. $this->autolinkActiveUsersOnly = $active;
  189. $this->target = null;
  190. $this->external = null;
  191. return $this;
  192. }
  193. /**
  194. * CSS class for auto-linked URLs.
  195. *
  196. * @return string CSS class for URL links.
  197. */
  198. public function getURLClass()
  199. {
  200. return $this->class_url;
  201. }
  202. /**
  203. * CSS class for auto-linked URLs.
  204. *
  205. * @param string $v CSS class for URL links.
  206. *
  207. * @return Autolink Fluid method chaining.
  208. */
  209. public function setURLClass($v)
  210. {
  211. $this->class_url = trim($v);
  212. return $this;
  213. }
  214. /**
  215. * CSS class for auto-linked username URLs.
  216. *
  217. * @return string CSS class for username links.
  218. */
  219. public function getUsernameClass()
  220. {
  221. return $this->class_user;
  222. }
  223. /**
  224. * CSS class for auto-linked username URLs.
  225. *
  226. * @param string $v CSS class for username links.
  227. *
  228. * @return Autolink Fluid method chaining.
  229. */
  230. public function setUsernameClass($v)
  231. {
  232. $this->class_user = trim($v);
  233. return $this;
  234. }
  235. /**
  236. * CSS class for auto-linked username/list URLs.
  237. *
  238. * @return string CSS class for username/list links.
  239. */
  240. public function getListClass()
  241. {
  242. return $this->class_list;
  243. }
  244. /**
  245. * CSS class for auto-linked username/list URLs.
  246. *
  247. * @param string $v CSS class for username/list links.
  248. *
  249. * @return Autolink Fluid method chaining.
  250. */
  251. public function setListClass($v)
  252. {
  253. $this->class_list = trim($v);
  254. return $this;
  255. }
  256. /**
  257. * CSS class for auto-linked hashtag URLs.
  258. *
  259. * @return string CSS class for hashtag links.
  260. */
  261. public function getHashtagClass()
  262. {
  263. return $this->class_hash;
  264. }
  265. /**
  266. * CSS class for auto-linked hashtag URLs.
  267. *
  268. * @param string $v CSS class for hashtag links.
  269. *
  270. * @return Autolink Fluid method chaining.
  271. */
  272. public function setHashtagClass($v)
  273. {
  274. $this->class_hash = trim($v);
  275. return $this;
  276. }
  277. /**
  278. * CSS class for auto-linked cashtag URLs.
  279. *
  280. * @return string CSS class for cashtag links.
  281. */
  282. public function getCashtagClass()
  283. {
  284. return $this->class_cash;
  285. }
  286. /**
  287. * CSS class for auto-linked cashtag URLs.
  288. *
  289. * @param string $v CSS class for cashtag links.
  290. *
  291. * @return Autolink Fluid method chaining.
  292. */
  293. public function setCashtagClass($v)
  294. {
  295. $this->class_cash = trim($v);
  296. return $this;
  297. }
  298. /**
  299. * Whether to include the value 'nofollow' in the 'rel' attribute.
  300. *
  301. * @return bool Whether to add 'nofollow' to the 'rel' attribute.
  302. */
  303. public function getNoFollow()
  304. {
  305. return $this->nofollow;
  306. }
  307. /**
  308. * Whether to include the value 'nofollow' in the 'rel' attribute.
  309. *
  310. * @param bool $v The value to add to the 'target' attribute.
  311. *
  312. * @return Autolink Fluid method chaining.
  313. */
  314. public function setNoFollow($v)
  315. {
  316. $this->nofollow = $v;
  317. return $this;
  318. }
  319. /**
  320. * Whether to include the value 'external' in the 'rel' attribute.
  321. *
  322. * Often this is used to be matched on in JavaScript for dynamically adding
  323. * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
  324. * been undeprecated and thus the 'target' attribute can be used. If this is
  325. * set to false then the 'target' attribute will be output.
  326. *
  327. * @return bool Whether to add 'external' to the 'rel' attribute.
  328. */
  329. public function getExternal()
  330. {
  331. return $this->external;
  332. }
  333. /**
  334. * Whether to include the value 'external' in the 'rel' attribute.
  335. *
  336. * Often this is used to be matched on in JavaScript for dynamically adding
  337. * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
  338. * been undeprecated and thus the 'target' attribute can be used. If this is
  339. * set to false then the 'target' attribute will be output.
  340. *
  341. * @param bool $v The value to add to the 'target' attribute.
  342. *
  343. * @return Autolink Fluid method chaining.
  344. */
  345. public function setExternal($v)
  346. {
  347. $this->external = $v;
  348. return $this;
  349. }
  350. /**
  351. * The scope to open the link in.
  352. *
  353. * Support for the 'target' attribute was deprecated in HTML 4.01 but has
  354. * since been reinstated in HTML 5. To output the 'target' attribute you
  355. * must disable the adding of the string 'external' to the 'rel' attribute.
  356. *
  357. * @return string The value to add to the 'target' attribute.
  358. */
  359. public function getTarget()
  360. {
  361. return $this->target;
  362. }
  363. /**
  364. * The scope to open the link in.
  365. *
  366. * Support for the 'target' attribute was deprecated in HTML 4.01 but has
  367. * since been reinstated in HTML 5. To output the 'target' attribute you
  368. * must disable the adding of the string 'external' to the 'rel' attribute.
  369. *
  370. * @param string $v The value to add to the 'target' attribute.
  371. *
  372. * @return Autolink Fluid method chaining.
  373. */
  374. public function setTarget($v)
  375. {
  376. $this->target = trim($v);
  377. return $this;
  378. }
  379. /**
  380. * Autolink with entities.
  381. *
  382. * @param string $tweet
  383. * @param array $entities
  384. *
  385. * @return string
  386. *
  387. * @since 1.1.0
  388. */
  389. public function autoLinkEntities($tweet = null, $entities = null)
  390. {
  391. if (is_null($tweet)) {
  392. $tweet = $this->tweet;
  393. }
  394. $text = '';
  395. $beginIndex = 0;
  396. foreach ($entities as $entity) {
  397. if (isset($entity['screen_name'])) {
  398. if(Str::startsWith($entity['screen_name'], '@')) {
  399. $text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
  400. } else {
  401. $text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
  402. }
  403. } else {
  404. $text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
  405. }
  406. if (isset($entity['url'])) {
  407. $text .= $this->linkToUrl($entity);
  408. } elseif (isset($entity['hashtag'])) {
  409. $text .= $this->linkToHashtag($entity, $tweet);
  410. } elseif (isset($entity['screen_name'])) {
  411. $text .= $this->linkToMentionAndList($entity);
  412. } elseif (isset($entity['cashtag'])) {
  413. $text .= $this->linkToCashtag($entity, $tweet);
  414. }
  415. $beginIndex = $entity['indices'][1];
  416. }
  417. $text .= StringUtils::substr($tweet, $beginIndex, StringUtils::strlen($tweet));
  418. return $text;
  419. }
  420. /**
  421. * Auto-link hashtags, URLs, usernames and lists, with JSON entities.
  422. *
  423. * @param string The tweet to be converted
  424. * @param mixed The entities info
  425. *
  426. * @return string that auto-link HTML added
  427. *
  428. * @since 1.1.0
  429. */
  430. public function autoLinkWithJson($tweet = null, $json = null)
  431. {
  432. // concatenate entities
  433. $entities = [];
  434. if (is_object($json)) {
  435. $json = $this->object2array($json);
  436. }
  437. if (is_array($json)) {
  438. foreach ($json as $key => $vals) {
  439. $entities = array_merge($entities, $json[$key]);
  440. }
  441. }
  442. // map JSON entity to twitter-text entity
  443. foreach ($entities as $idx => $entity) {
  444. if (!empty($entity['text'])) {
  445. $entities[$idx]['hashtag'] = $entity['text'];
  446. }
  447. }
  448. $entities = $this->extractor->removeOverlappingEntities($entities);
  449. return $this->autoLinkEntities($tweet, $entities);
  450. }
  451. /**
  452. * convert Object to Array.
  453. *
  454. * @param mixed $obj
  455. *
  456. * @return array
  457. */
  458. protected function object2array($obj)
  459. {
  460. $array = (array) $obj;
  461. foreach ($array as $key => $var) {
  462. if (is_object($var) || is_array($var)) {
  463. $array[$key] = $this->object2array($var);
  464. }
  465. }
  466. return $array;
  467. }
  468. /**
  469. * Auto-link hashtags, URLs, usernames and lists.
  470. *
  471. * @param string The tweet to be converted
  472. *
  473. * @return string that auto-link HTML added
  474. *
  475. * @since 1.1.0
  476. */
  477. public function autoLink($tweet = null)
  478. {
  479. if (is_null($tweet)) {
  480. $tweet = $this->tweet;
  481. }
  482. $entities = $this->extractor->extractURLWithoutProtocol(false)->extractEntitiesWithIndices($tweet);
  483. return $this->autoLinkEntities($tweet, $entities);
  484. }
  485. /**
  486. * Auto-link the @username and @username/list references in the provided text. Links to @username references will
  487. * have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
  488. * added.
  489. *
  490. * @return string that auto-link HTML added
  491. *
  492. * @since 1.1.0
  493. */
  494. public function autoLinkUsernamesAndLists($tweet = null)
  495. {
  496. if (is_null($tweet)) {
  497. $tweet = $this->tweet;
  498. }
  499. $entities = $this->extractor->extractMentionsOrListsWithIndices($tweet);
  500. if($this->autolinkActiveUsersOnly == true) {
  501. $entities = collect($entities)
  502. ->filter(function($entity) {
  503. return AutolinkService::mentionedUsernameExists($entity['screen_name']);
  504. })
  505. ->toArray();
  506. }
  507. return $this->autoLinkEntities($tweet, $entities);
  508. }
  509. /**
  510. * Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
  511. * added.
  512. *
  513. * @return string that auto-link HTML added
  514. *
  515. * @since 1.1.0
  516. */
  517. public function autoLinkHashtags($tweet = null)
  518. {
  519. if (is_null($tweet)) {
  520. $tweet = $this->tweet;
  521. }
  522. $entities = $this->extractor->extractHashtagsWithIndices($tweet);
  523. return $this->autoLinkEntities($tweet, $entities);
  524. }
  525. /**
  526. * Auto-link URLs in the Tweet text provided.
  527. * <p/>
  528. * This only auto-links URLs with protocol.
  529. *
  530. * @return string that auto-link HTML added
  531. *
  532. * @since 1.1.0
  533. */
  534. public function autoLinkURLs($tweet = null)
  535. {
  536. if (is_null($tweet)) {
  537. $tweet = $this->tweet;
  538. }
  539. $entities = $this->extractor->extractURLWithoutProtocol(false)->extractURLsWithIndices($tweet);
  540. return $this->autoLinkEntities($tweet, $entities);
  541. }
  542. /**
  543. * Auto-link $cashtag references in the provided Tweet text. The $cashtag links will have the cashtagClass CSS class
  544. * added.
  545. *
  546. * @return string that auto-link HTML added
  547. *
  548. * @since 1.1.0
  549. */
  550. public function autoLinkCashtags($tweet = null)
  551. {
  552. if (is_null($tweet)) {
  553. $tweet = $this->tweet;
  554. }
  555. $entities = $this->extractor->extractCashtagsWithIndices($tweet);
  556. return $this->autoLinkEntities($tweet, $entities);
  557. }
  558. public function linkToUrl($entity)
  559. {
  560. if (!empty($this->class_url)) {
  561. $attributes['class'] = $this->class_url;
  562. }
  563. $attributes['href'] = $entity['url'];
  564. $linkText = $this->escapeHTML($entity['url']);
  565. if (!empty($entity['display_url']) && !empty($entity['expanded_url'])) {
  566. // Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
  567. // should contain the full original URL (expanded_url), not the display URL.
  568. //
  569. // Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
  570. // font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
  571. // Elements with font-size:0 get copied even though they are not visible.
  572. // Note that display:none doesn't work here. Elements with display:none don't get copied.
  573. //
  574. // Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
  575. // wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
  576. // everything with the tco-ellipsis class.
  577. //
  578. // As an example: The user tweets "hi http://longdomainname.com/foo"
  579. // This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
  580. // This will get rendered as:
  581. // <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
  582. // …
  583. // <!-- There's a chance the onCopy event handler might not fire. In case that happens,
  584. // we include an &nbsp; here so that the … doesn't bump up against the URL and ruin it.
  585. // The &nbsp; is inside the tco-ellipsis span so that when the onCopy handler *does*
  586. // fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
  587. // e.g. "hi http://longdomainname.com/foo".
  588. // <span style='font-size:0'>&nbsp;</span>
  589. // </span>
  590. // <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
  591. // http://longdomai
  592. // </span>
  593. // <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
  594. // nname.com/foo
  595. // </span>
  596. // <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
  597. // <span style='font-size:0'>&nbsp;</span>
  598. // …
  599. // </span>
  600. //
  601. // Exception: pic.socialhub.dev images, for which expandedUrl = "https://socialhub.dev/#!/username/status/1234/photo/1
  602. // For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
  603. // For a pic.socialhub.dev URL, the only elided part will be the "https://", so this is fine.
  604. $displayURL = $entity['display_url'];
  605. $expandedURL = $entity['expanded_url'];
  606. $displayURLSansEllipses = preg_replace('/…/u', '', $displayURL);
  607. $diplayURLIndexInExpandedURL = mb_strpos($expandedURL, $displayURLSansEllipses);
  608. if ($diplayURLIndexInExpandedURL !== false) {
  609. $beforeDisplayURL = mb_substr($expandedURL, 0, $diplayURLIndexInExpandedURL);
  610. $afterDisplayURL = mb_substr($expandedURL, $diplayURLIndexInExpandedURL + mb_strlen($displayURLSansEllipses));
  611. $precedingEllipsis = (preg_match('/\A…/u', $displayURL)) ? '…' : '';
  612. $followingEllipsis = (preg_match('/…\z/u', $displayURL)) ? '…' : '';
  613. $invisibleSpan = "<span {$this->invisibleTagAttrs}>";
  614. $linkText = "<span class='tco-ellipsis'>{$precedingEllipsis}{$invisibleSpan}&nbsp;</span></span>";
  615. $linkText .= "{$invisibleSpan}{$this->escapeHTML($beforeDisplayURL)}</span>";
  616. $linkText .= "<span class='js-display-url'>{$this->escapeHTML($displayURLSansEllipses)}</span>";
  617. $linkText .= "{$invisibleSpan}{$this->escapeHTML($afterDisplayURL)}</span>";
  618. $linkText .= "<span class='tco-ellipsis'>{$invisibleSpan}&nbsp;</span>{$followingEllipsis}</span>";
  619. } else {
  620. $linkText = $entity['display_url'];
  621. }
  622. $attributes['title'] = $entity['expanded_url'];
  623. } elseif (!empty($entity['display_url'])) {
  624. $linkText = $entity['display_url'];
  625. }
  626. return $this->linkToText($entity, $linkText, $attributes);
  627. }
  628. /**
  629. * @param array $entity
  630. * @param string $tweet
  631. *
  632. * @return string
  633. *
  634. * @since 1.1.0
  635. */
  636. public function linkToHashtag($entity, $tweet = null)
  637. {
  638. if (is_null($tweet)) {
  639. $tweet = $this->tweet;
  640. }
  641. $this->target = false;
  642. $attributes = [];
  643. $class = [];
  644. $hash = StringUtils::substr($tweet, $entity['indices'][0], 1);
  645. $linkText = $hash.$entity['hashtag'];
  646. $attributes['href'] = $this->url_base_hash.$entity['hashtag'].'?src=hash';
  647. $attributes['title'] = '#'.$entity['hashtag'];
  648. if (!empty($this->class_hash)) {
  649. $class[] = $this->class_hash;
  650. }
  651. if (preg_match(self::$patterns['rtl_chars'], $linkText)) {
  652. $class[] = 'rtl';
  653. }
  654. if (!empty($class)) {
  655. $attributes['class'] = implode(' ', $class);
  656. }
  657. return $this->linkToText($entity, $linkText, $attributes);
  658. }
  659. /**
  660. * @param array $entity
  661. *
  662. * @return string
  663. *
  664. * @since 1.1.0
  665. */
  666. public function linkToMentionAndList($entity)
  667. {
  668. $attributes = [];
  669. $screen_name = $entity['screen_name'];
  670. if($this->autolinkActiveUsersOnly == true) {
  671. if(!AutolinkService::mentionedUsernameExists($screen_name)) {
  672. return Str::of($screen_name)->startsWith('@') ? $screen_name : "@{$screen_name}";
  673. }
  674. }
  675. if (!empty($entity['list_slug'])) {
  676. // Replace the list and username
  677. $linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
  678. $class = $this->class_list;
  679. $url = $this->url_base_list.$screen_name;
  680. } else {
  681. // Replace the username
  682. $linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
  683. $class = $this->class_user;
  684. $url = $this->url_base_user . $screen_name;
  685. }
  686. if (!empty($class)) {
  687. $attributes['class'] = $class;
  688. }
  689. $attributes['href'] = $url;
  690. return $this->linkToText($entity, $linkText, $attributes);
  691. }
  692. /**
  693. * @param array $entity
  694. * @param string $tweet
  695. *
  696. * @return string
  697. *
  698. * @since 1.1.0
  699. */
  700. public function linkToCashtag($entity, $tweet = null)
  701. {
  702. if (is_null($tweet)) {
  703. $tweet = $this->tweet;
  704. }
  705. $attributes = [];
  706. $doller = StringUtils::substr($tweet, $entity['indices'][0], 1);
  707. $linkText = $doller.$entity['cashtag'];
  708. $attributes['href'] = $this->url_base_cash.$entity['cashtag'];
  709. $attributes['title'] = $linkText;
  710. if (!empty($this->class_cash)) {
  711. $attributes['class'] = $this->class_cash;
  712. }
  713. return $this->linkToText($entity, $linkText, $attributes);
  714. }
  715. /**
  716. * @param array $entity
  717. * @param string $text
  718. * @param array $attributes
  719. *
  720. * @return string
  721. *
  722. * @since 1.1.0
  723. */
  724. public function linkToText(array $entity, $text, $attributes = [])
  725. {
  726. $rel = [];
  727. if ($this->external) {
  728. $rel[] = 'external';
  729. }
  730. if ($this->nofollow) {
  731. $rel[] = 'nofollow';
  732. }
  733. if ($this->noopener) {
  734. $rel[] = 'noopener';
  735. }
  736. if (!empty($rel)) {
  737. $attributes['rel'] = implode(' ', $rel);
  738. }
  739. if ($this->target) {
  740. $attributes['target'] = $this->target;
  741. }
  742. $link = '<a';
  743. foreach ($attributes as $key => $val) {
  744. $link .= ' '.$key.'="'.$this->escapeHTML($val).'"';
  745. }
  746. $link .= '>'.$text.'</a>';
  747. return $link;
  748. }
  749. /**
  750. * html escape.
  751. *
  752. * @param string $text
  753. *
  754. * @return string
  755. */
  756. protected function escapeHTML($text)
  757. {
  758. return htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false);
  759. }
  760. }